brain-wiki 0.1.0.dev0__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.
Files changed (141) hide show
  1. brain_wiki-0.1.0.dev0.dist-info/METADATA +231 -0
  2. brain_wiki-0.1.0.dev0.dist-info/RECORD +141 -0
  3. brain_wiki-0.1.0.dev0.dist-info/WHEEL +4 -0
  4. brain_wiki-0.1.0.dev0.dist-info/entry_points.txt +4 -0
  5. brain_wiki-0.1.0.dev0.dist-info/licenses/LICENSE +202 -0
  6. brainwiki/__init__.py +17 -0
  7. brainwiki/__main__.py +5 -0
  8. brainwiki/assets/plugin/.claude-plugin/marketplace.json +16 -0
  9. brainwiki/assets/plugin/.claude-plugin/plugin.json +19 -0
  10. brainwiki/assets/plugin/.mcp.json +16 -0
  11. brainwiki/assets/plugin/mcp.json +17 -0
  12. brainwiki/assets/plugin/mcp_config.json +15 -0
  13. brainwiki/assets/plugin/plugin.json +37 -0
  14. brainwiki/assets/plugin/rules/brain-wiki.md +30 -0
  15. brainwiki/assets/plugin/server.json +30 -0
  16. brainwiki/assets/plugin/skills/brain/SKILL.md +145 -0
  17. brainwiki/assets/plugin/skills/brain/references/format.md +149 -0
  18. brainwiki/assets/plugin/skills/brain-maintain/SKILL.md +111 -0
  19. brainwiki/assets/plugin/skills/brain-maintain/references/consolidate.md +91 -0
  20. brainwiki/assets/plugin/skills/brain-maintain/references/harvest.md +144 -0
  21. brainwiki/assets/plugin/skills/brain-setup/SKILL.md +184 -0
  22. brainwiki/assets/plugin/skills/brain-setup/references/hosts.md +88 -0
  23. brainwiki/assets/plugin/skills/brain-setup/references/privacy.md +67 -0
  24. brainwiki/assets/prompts/consolidate.md +89 -0
  25. brainwiki/assets/prompts/distill.md +96 -0
  26. brainwiki/assets/prompts/harvest.md +142 -0
  27. brainwiki/bench/__init__.py +8 -0
  28. brainwiki/bench/_generate.py +440 -0
  29. brainwiki/bench/_report.py +166 -0
  30. brainwiki/bench/_suites.py +959 -0
  31. brainwiki/cli/__init__.py +7 -0
  32. brainwiki/cli/_cmd_bench.py +101 -0
  33. brainwiki/cli/_cmd_browse.py +281 -0
  34. brainwiki/cli/_cmd_consolidate.py +108 -0
  35. brainwiki/cli/_cmd_core.py +155 -0
  36. brainwiki/cli/_cmd_harvest.py +137 -0
  37. brainwiki/cli/_cmd_hosts.py +52 -0
  38. brainwiki/cli/_cmd_import.py +149 -0
  39. brainwiki/cli/_cmd_lint.py +117 -0
  40. brainwiki/cli/_cmd_read.py +131 -0
  41. brainwiki/cli/_cmd_runs.py +66 -0
  42. brainwiki/cli/_cmd_viz.py +57 -0
  43. brainwiki/cli/_cmd_write.py +122 -0
  44. brainwiki/cli/_main.py +511 -0
  45. brainwiki/cli/_output.py +19 -0
  46. brainwiki/config/__init__.py +56 -0
  47. brainwiki/config/_dirs.py +69 -0
  48. brainwiki/config/_manifest.py +307 -0
  49. brainwiki/config/_resolve.py +169 -0
  50. brainwiki/config/_user.py +183 -0
  51. brainwiki/consolidate/__init__.py +19 -0
  52. brainwiki/consolidate/_apply.py +408 -0
  53. brainwiki/consolidate/_packet.py +489 -0
  54. brainwiki/consolidate/_pipeline.py +215 -0
  55. brainwiki/consolidate/_presets.py +230 -0
  56. brainwiki/consolidate/_review.py +104 -0
  57. brainwiki/consolidate/_schema.py +153 -0
  58. brainwiki/consolidate/_validate.py +252 -0
  59. brainwiki/doctor.py +216 -0
  60. brainwiki/engine.py +80 -0
  61. brainwiki/errors.py +81 -0
  62. brainwiki/format/__init__.py +112 -0
  63. brainwiki/format/_agents_md.py +26 -0
  64. brainwiki/format/_claims.py +95 -0
  65. brainwiki/format/_document.py +203 -0
  66. brainwiki/format/_edit.py +94 -0
  67. brainwiki/format/_episode.py +67 -0
  68. brainwiki/format/_fields.py +350 -0
  69. brainwiki/format/_frontmatter.py +175 -0
  70. brainwiki/format/_links.py +72 -0
  71. brainwiki/format/_meta.py +67 -0
  72. brainwiki/format/_normalize.py +70 -0
  73. brainwiki/format/_relations.py +211 -0
  74. brainwiki/format/_rules.py +41 -0
  75. brainwiki/harvest.py +414 -0
  76. brainwiki/hosts/__init__.py +22 -0
  77. brainwiki/hosts/_antigravity.py +163 -0
  78. brainwiki/hosts/_claude_code.py +174 -0
  79. brainwiki/hosts/_codex.py +149 -0
  80. brainwiki/hosts/_connect.py +443 -0
  81. brainwiki/hosts/_detect.py +39 -0
  82. brainwiki/hosts/_instructions.py +220 -0
  83. brainwiki/hosts/_sessions.py +176 -0
  84. brainwiki/ids.py +69 -0
  85. brainwiki/importer/__init__.py +17 -0
  86. brainwiki/importer/_episodes.py +112 -0
  87. brainwiki/importer/_into.py +102 -0
  88. brainwiki/importer/_normalize.py +64 -0
  89. brainwiki/importer/_overlay.py +199 -0
  90. brainwiki/importer/_survey.py +167 -0
  91. brainwiki/index/__init__.py +44 -0
  92. brainwiki/index/_brains.py +119 -0
  93. brainwiki/index/_db.py +131 -0
  94. brainwiki/index/_guards.py +154 -0
  95. brainwiki/index/_neighbors.py +174 -0
  96. brainwiki/index/_orient.py +400 -0
  97. brainwiki/index/_read.py +285 -0
  98. brainwiki/index/_resolver.py +147 -0
  99. brainwiki/index/_scanner.py +665 -0
  100. brainwiki/index/_schema.py +164 -0
  101. brainwiki/index/_search.py +659 -0
  102. brainwiki/index/_topics.py +253 -0
  103. brainwiki/index/_written.py +29 -0
  104. brainwiki/lifecycle.py +158 -0
  105. brainwiki/lint/__init__.py +20 -0
  106. brainwiki/lint/_catalog.py +85 -0
  107. brainwiki/lint/_cross.py +649 -0
  108. brainwiki/lint/_files.py +483 -0
  109. brainwiki/lint/_fixes.py +87 -0
  110. brainwiki/lint/_hook.py +54 -0
  111. brainwiki/lint/_model.py +103 -0
  112. brainwiki/lint/_reports.py +242 -0
  113. brainwiki/lint/_runner.py +156 -0
  114. brainwiki/logs.py +34 -0
  115. brainwiki/mcp/__init__.py +7 -0
  116. brainwiki/mcp/_server.py +917 -0
  117. brainwiki/origin.py +97 -0
  118. brainwiki/runs/__init__.py +21 -0
  119. brainwiki/runs/_git.py +89 -0
  120. brainwiki/runs/_lock.py +42 -0
  121. brainwiki/runs/_record.py +145 -0
  122. brainwiki/runs/_transaction.py +441 -0
  123. brainwiki/runs/_undo.py +159 -0
  124. brainwiki/setup_wizard.py +143 -0
  125. brainwiki/store/__init__.py +7 -0
  126. brainwiki/store/_local.py +153 -0
  127. brainwiki/viz/__init__.py +9 -0
  128. brainwiki/viz/_export.py +414 -0
  129. brainwiki/viz/_lod.py +455 -0
  130. brainwiki/viz/_server.py +327 -0
  131. brainwiki/viz/app/BUILD_INFO.json +5 -0
  132. brainwiki/viz/app/THIRD_PARTY_NOTICES.txt +1519 -0
  133. brainwiki/viz/app/assets/index-gK1pnQdO.js +4496 -0
  134. brainwiki/viz/app/assets/style-Cg8NUCst.css +1 -0
  135. brainwiki/viz/app/index.html +14 -0
  136. brainwiki/write/__init__.py +23 -0
  137. brainwiki/write/_edits.py +730 -0
  138. brainwiki/write/_episode.py +195 -0
  139. brainwiki/write/_move.py +181 -0
  140. brainwiki/write/_rules.py +204 -0
  141. brainwiki/write/_secrets.py +108 -0
@@ -0,0 +1,7 @@
1
+ """Command-line interface (design doc 07 §2)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from brainwiki.cli._main import main
6
+
7
+ __all__ = ["main"]
@@ -0,0 +1,101 @@
1
+ """bench: run the evaluation suites (design doc 11)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ import time
9
+ from collections.abc import Iterator
10
+ from contextlib import contextmanager
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from brainwiki.errors import UsageError
15
+
16
+ _SUITES = ("retrieval", "lint", "consolidation", "perf", "agent")
17
+ _DEFAULT_SUITES = ["retrieval", "lint", "perf"]
18
+ _ENV = ("BRAIN_WIKI_CONFIG", "BRAIN_WIKI_STATE_DIR", "BRAIN_WIKI_CACHE_DIR", "BRAIN_WIKI_DATA_DIR")
19
+
20
+
21
+ def _parse_sizes(raw: str) -> list[int]:
22
+ sizes = []
23
+ for item in (s.strip().lower() for s in raw.split(",") if s.strip()):
24
+ try:
25
+ sizes.append(int(float(item[:-1]) * 1000) if item.endswith("k") else int(item))
26
+ except ValueError as exc:
27
+ raise UsageError(f"bad size {item!r}; use e.g. 1k, 10k or 200") from exc
28
+ if not sizes or min(sizes) < 50:
29
+ raise UsageError("sizes must be at least 50 notes")
30
+ return sizes
31
+
32
+
33
+ @contextmanager
34
+ def _isolated_state(base: Path) -> Iterator[None]:
35
+ """Keep bench indexes, runs, and user config away from the user's own brain-wiki state."""
36
+ saved = {k: os.environ.get(k) for k in (*_ENV, "BRAIN_WIKI_BRAIN")}
37
+ os.environ["BRAIN_WIKI_CONFIG"] = str(base / "config.toml")
38
+ for key, sub in zip(_ENV[1:], ("state", "cache", "data"), strict=True):
39
+ os.environ[key] = str(base / sub)
40
+ os.environ.pop("BRAIN_WIKI_BRAIN", None)
41
+ try:
42
+ yield
43
+ finally:
44
+ for key, value in saved.items():
45
+ if value is None:
46
+ os.environ.pop(key, None)
47
+ else:
48
+ os.environ[key] = value
49
+
50
+
51
+ def bench(args: Any) -> int:
52
+ from brainwiki.bench import report, suites
53
+
54
+ if args.compare:
55
+ text, regressions = report.compare(Path(args.compare[0]), Path(args.compare[1]))
56
+ print(text)
57
+ return 1 if regressions else 0
58
+ names = args.suites or _DEFAULT_SUITES
59
+ unknown = [n for n in names if n not in _SUITES]
60
+ if unknown:
61
+ raise UsageError(f"unknown suite(s) {', '.join(unknown)}; choose from {', '.join(_SUITES)}")
62
+ sizes = _parse_sizes(args.sizes)
63
+ backends = [b.strip() for b in (args.backends or "").split(",") if b.strip()]
64
+ out = Path(args.out) if args.out else Path(tempfile.mkdtemp(prefix="brain-bench-"))
65
+ work = out / "work"
66
+ if work.exists():
67
+ shutil.rmtree(work) # generated brains from an earlier run in the same --out
68
+ work.mkdir(parents=True)
69
+
70
+ results = []
71
+ with _isolated_state(work / ".state"):
72
+ for name in names:
73
+ started = time.monotonic()
74
+ if name in ("retrieval", "perf"):
75
+ result = getattr(suites, name)(work, sizes[0])
76
+ for size in sizes[1:]:
77
+ extra = getattr(suites, name)(work, size)
78
+ result.metrics += extra.metrics
79
+ result.details.update({f"{k}@{size}": v for k, v in extra.details.items()})
80
+ elif name == "lint":
81
+ result = suites.lint_suite(work)
82
+ else:
83
+ result = getattr(suites, name)(work, backends)
84
+ result.details["seconds"] = round(time.monotonic() - started, 1)
85
+ results.append(result)
86
+ if not args.quiet:
87
+ print(f"{name}: {result.status} ({result.details['seconds']} s)", flush=True)
88
+
89
+ command = " ".join(["brain bench", *names, "--sizes", args.sizes])
90
+ if backends:
91
+ command += " --backends " + ",".join(backends)
92
+ data = report.build(results, command=command, sizes=sizes, backends=backends)
93
+ results_path, report_path = report.write(out, data)
94
+ print(report.table(data))
95
+ failures = report.hard_failures(data)
96
+ if failures:
97
+ print(f"\n{len(failures)} hard target(s) failed:")
98
+ for m in failures[:20]:
99
+ print(f" {m['suite']} {m['case']} {m['metric']} = {m['value']} (target {m['target']})")
100
+ print(f"\nResults: {results_path}\nReport: {report_path}")
101
+ return 1 if failures else 0
@@ -0,0 +1,281 @@
1
+ """Small browsing commands in the Obsidian CLI's style (design doc 07 §2.2).
2
+
3
+ Targets resolve like wikilinks; every line names the exact path, the handle for the next
4
+ command. Listings share --count, --paths, --limit, and --cursor.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from typing import Any
11
+
12
+ from brainwiki.cli._output import emit
13
+ from brainwiki.engine import Engine, open_engine
14
+ from brainwiki.index import node_filters, parse_cursor
15
+
16
+ _NOTE_KINDS = ["note", "hub"]
17
+ _STATUSES = ["active", "superseded"]
18
+
19
+
20
+ def _engine(args: Any) -> Engine:
21
+ return open_engine(args.brain, host=getattr(args, "as_host", None))
22
+
23
+
24
+ def _note_where(engine: Engine) -> tuple[str, list[Any]]:
25
+ return node_filters(engine, _NOTE_KINDS, _STATUSES, None)
26
+
27
+
28
+ def _listing(
29
+ args: Any,
30
+ rows: list[dict[str, Any]],
31
+ line: Callable[[dict[str, Any]], str],
32
+ *,
33
+ empty: str,
34
+ path_key: str = "path",
35
+ ) -> int:
36
+ """Print a list with the shared flags; JSON carries the page plus the total."""
37
+ total = len(rows)
38
+ offset = parse_cursor(getattr(args, "cursor", None))
39
+ limit = getattr(args, "limit", None) or total
40
+ page = rows[offset : offset + limit]
41
+ more = offset + len(page) < total
42
+ data = {"total": total, "items": page, "next_cursor": str(offset + len(page)) if more else None}
43
+ if getattr(args, "count", False):
44
+ emit(args, data, str(total))
45
+ return 0
46
+ if getattr(args, "paths", False):
47
+ text = "\n".join(dict.fromkeys(str(r[path_key]) for r in page if r.get(path_key)))
48
+ else:
49
+ text = "\n".join(line(r) for r in page) or empty
50
+ if more:
51
+ text += f"\n… {total - offset - len(page)} more (--cursor {offset + len(page)})"
52
+ emit(args, data, text)
53
+ return 0
54
+
55
+
56
+ def topics(args: Any) -> int:
57
+ """The topic tree: slug, title, notes, and sub-topics indented below."""
58
+ from brainwiki.errors import NotFound
59
+ from brainwiki.index import topic_rows
60
+
61
+ with _engine(args) as engine:
62
+ engine.scan()
63
+ rows = {r["slug"]: r for r in topic_rows(engine)}
64
+ if args.topic and args.topic not in rows:
65
+ raise NotFound(f"no topic {args.topic!r}")
66
+ starts = (
67
+ [args.topic] if args.topic else sorted(s for s, r in rows.items() if r["parent"] is None)
68
+ )
69
+ items: list[dict[str, Any]] = []
70
+
71
+ def walk(slug: str, depth: int) -> None:
72
+ items.append({**rows[slug], "depth": depth})
73
+ for child in sorted(s for s, r in rows.items() if r["parent"] == slug):
74
+ walk(child, depth + 1)
75
+
76
+ for slug in starts:
77
+ walk(slug, 0)
78
+
79
+ def line(r: dict[str, Any]) -> str:
80
+ own = f"{r['notes']} note{'' if r['notes'] == 1 else 's'}"
81
+ if r["sub_topics"]:
82
+ own += f", {r['total_notes']} with sub-topics"
83
+ return f"{' ' * r['depth']}- {r['slug']}: {r['title']} ({own}) [{r['path']}]"
84
+
85
+ return _listing(args, items, line, empty="(no topics)")
86
+
87
+
88
+ def notes(args: Any) -> int:
89
+ from brainwiki.index import format_search_text, search
90
+
91
+ with _engine(args) as engine:
92
+ result = search(
93
+ engine,
94
+ "",
95
+ limit=args.limit,
96
+ topics=args.topic,
97
+ types=args.types,
98
+ tags=args.tags,
99
+ sort=args.sort,
100
+ cursor=args.cursor,
101
+ count_only=args.count,
102
+ facets=args.facets,
103
+ intent="none",
104
+ )
105
+ if args.count and not args.json and not args.facets:
106
+ print(result["total"])
107
+ return 0
108
+ emit(args, result, format_search_text(result, "paths" if args.paths else "text"))
109
+ return 0
110
+
111
+
112
+ def recent(args: Any) -> int:
113
+ args.topic, args.types, args.tags, args.sort, args.facets = None, None, None, "recent", False
114
+ from brainwiki.index import format_search_text, search
115
+
116
+ with _engine(args) as engine:
117
+ result = search(
118
+ engine,
119
+ "",
120
+ limit=args.limit,
121
+ since=args.since,
122
+ cursor=args.cursor,
123
+ count_only=args.count,
124
+ )
125
+ if args.count and not args.json:
126
+ print(result["total"])
127
+ return 0
128
+ emit(args, result, format_search_text(result, "paths" if args.paths else "text"))
129
+ return 0
130
+
131
+
132
+ def outline(args: Any) -> int:
133
+ from brainwiki.format import parse_document
134
+ from brainwiki.index import resolve_target
135
+
136
+ with _engine(args) as engine:
137
+ node = resolve_target(engine, args.target)
138
+ doc = parse_document(node["path"], engine.store.read_text(node["path"]))
139
+ headings = [{"line": line, "level": level, "text": text} for line, level, text in doc.headings]
140
+ text = "\n".join(
141
+ [f"{node['title']} [{node['path']}]"]
142
+ + [f"{' ' * (h['level'] - 1)}- {h['text']} (line {h['line']})" for h in headings]
143
+ )
144
+ emit(args, {"path": node["path"], "headings": headings}, text)
145
+ return 0
146
+
147
+
148
+ def links(args: Any) -> int:
149
+ """Everything a note links to: typed relations and body links, resolved or not."""
150
+ from brainwiki.index import resolve_target
151
+
152
+ with _engine(args) as engine:
153
+ node = resolve_target(engine, args.target)
154
+ rows = []
155
+ for edge in engine.db.execute(
156
+ """SELECT e.verb, e.rel, e.raw_target, e.line, n.id, n.title, n.path
157
+ FROM edges e LEFT JOIN nodes n ON n.id = e.other
158
+ WHERE e.declared_in = ? ORDER BY e.line""",
159
+ (node["id"],),
160
+ ):
161
+ rows.append(
162
+ {
163
+ "relation": edge["verb"],
164
+ "target": edge["raw_target"],
165
+ "line": edge["line"],
166
+ "id": edge["id"],
167
+ "title": edge["title"],
168
+ "path": edge["path"],
169
+ "resolved": edge["id"] is not None,
170
+ }
171
+ )
172
+ return _listing(
173
+ args,
174
+ rows,
175
+ lambda r: (
176
+ f'- {r["relation"]} "{r["title"]}" ({r["path"]})'
177
+ if r["path"]
178
+ else f"- {r['relation']} [[{r['target']}]]"
179
+ + ("" if r["resolved"] else " (unresolved)")
180
+ + f" line {r['line']}"
181
+ ),
182
+ empty="No links.",
183
+ )
184
+
185
+
186
+ def backlinks(args: Any) -> int:
187
+ from brainwiki.index import resolve_target
188
+
189
+ with _engine(args) as engine:
190
+ node = resolve_target(engine, args.target)
191
+ rows, seen = [], set()
192
+ for edge in engine.db.execute(
193
+ """SELECT e.verb, n.id, n.title, n.path
194
+ FROM edges e JOIN nodes n ON n.id = e.declared_in
195
+ WHERE e.other = ? AND e.declared_in != ?
196
+ ORDER BY e.rel = 'links_to', n.updated DESC""",
197
+ (node["id"], node["id"]),
198
+ ):
199
+ key = (edge["id"], edge["verb"])
200
+ if key in seen:
201
+ continue
202
+ seen.add(key)
203
+ rows.append(
204
+ {
205
+ "relation": edge["verb"],
206
+ "id": edge["id"],
207
+ "title": edge["title"],
208
+ "path": edge["path"],
209
+ }
210
+ )
211
+ return _listing(
212
+ args,
213
+ rows,
214
+ lambda r: f'- {r["relation"]} "{r["title"]}" ({r["path"]})',
215
+ empty="No backlinks.",
216
+ )
217
+
218
+
219
+ def _counts(args: Any, select: str, source: str, what: str) -> int:
220
+ with _engine(args) as engine:
221
+ where, params = _note_where(engine)
222
+ rows = [
223
+ {"name": r["k"], "count": r["c"]}
224
+ for r in engine.db.execute(
225
+ f"SELECT {select} AS k, COUNT(DISTINCT n.id) AS c FROM {source} WHERE {where} " # noqa: S608
226
+ f"AND {select} IS NOT NULL GROUP BY {select} ORDER BY c DESC, k",
227
+ params,
228
+ )
229
+ ]
230
+ return _listing(
231
+ args,
232
+ rows,
233
+ lambda r: f"{r['name']} {r['count']}" if args.counts else str(r["name"]),
234
+ empty=f"No {what}.",
235
+ path_key="name",
236
+ )
237
+
238
+
239
+ def tags(args: Any) -> int:
240
+ return _counts(args, "lower(t.value)", "nodes n, json_each(n.tags) t", "tags")
241
+
242
+
243
+ def types(args: Any) -> int:
244
+ return _counts(args, "n.type", "nodes n", "types")
245
+
246
+
247
+ def orphans(args: Any) -> int:
248
+ """Notes nothing else links to (hubs are entry points, so they are not listed)."""
249
+ with _engine(args) as engine:
250
+ where, params = _note_where(engine)
251
+ rows = []
252
+ for row in engine.db.execute(
253
+ f"""SELECT n.id, n.title, n.path, n.type FROM nodes n WHERE {where} AND n.kind = 'note'
254
+ AND NOT EXISTS (SELECT 1 FROM edges e JOIN nodes s ON s.id = e.declared_in
255
+ WHERE e.other = n.id AND e.declared_in != n.id)
256
+ ORDER BY n.path""", # noqa: S608
257
+ params,
258
+ ):
259
+ rows.append(dict(row))
260
+ return _listing(args, rows, lambda r: f'- "{r["title"]}" ({r["path"]})', empty="No orphans.")
261
+
262
+
263
+ def unresolved(args: Any) -> int:
264
+ """Links and relations that resolve to no note."""
265
+ with _engine(args) as engine:
266
+ where, params = _note_where(engine)
267
+ rows = [
268
+ {"target": r["raw_target"], "path": r["path"], "line": r["line"], "relation": r["verb"]}
269
+ for r in engine.db.execute(
270
+ f"""SELECT e.raw_target, e.line, e.verb, n.path FROM edges e
271
+ JOIN nodes n ON n.id = e.declared_in
272
+ WHERE e.other IS NULL AND {where} ORDER BY e.raw_target, n.path, e.line""", # noqa: S608
273
+ params,
274
+ )
275
+ ]
276
+ return _listing(
277
+ args,
278
+ rows,
279
+ lambda r: f"[[{r['target']}]] {r['path']}:{r['line']} ({r['relation']})",
280
+ empty="Every link resolves.",
281
+ )
@@ -0,0 +1,108 @@
1
+ """consolidate, review."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from brainwiki.cli._output import emit
11
+ from brainwiki.engine import open_engine
12
+ from brainwiki.errors import UsageError
13
+
14
+
15
+ def consolidate(args: Any) -> int:
16
+ from brainwiki.consolidate import pipeline
17
+
18
+ host = getattr(args, "as_host", None)
19
+ with open_engine(args.brain, host=host) as engine:
20
+ if args.apply_file:
21
+ if not args.run:
22
+ raise UsageError("--apply needs --run RUN (the id from --prepare)")
23
+ raw = (
24
+ sys.stdin.read()
25
+ if args.apply_file == "-"
26
+ else Path(args.apply_file).read_text(encoding="utf-8")
27
+ )
28
+ try:
29
+ payload = json.loads(raw)
30
+ except ValueError as exc:
31
+ raise UsageError(f"{args.apply_file} is not JSON: {exc}") from exc
32
+ result = pipeline.apply_run(
33
+ engine,
34
+ args.run,
35
+ payload,
36
+ engine.profile,
37
+ dry_run=args.dry_run,
38
+ agent=host,
39
+ backend="agent",
40
+ )
41
+ emit(args, result.to_json(), result.format_text())
42
+ return 0 if result.status in ("applied", "review", "dry_run") else 1
43
+ if args.prepare:
44
+ packet = pipeline.prepare(
45
+ engine, engine.profile, topic=args.topic, max_episodes=args.max_episodes
46
+ )
47
+ if packet is None:
48
+ emit(args, {"run": None, "pending": 0}, "Nothing to consolidate.")
49
+ return 0
50
+ text = pipeline.instructions() + "\n" + packet.model_text() + "\n"
51
+ if args.out:
52
+ Path(args.out).write_text(text, encoding="utf-8")
53
+ emit(args, {"run": packet.run, "out": args.out}, f"{packet.run}: wrote {args.out}")
54
+ else:
55
+ emit(
56
+ args,
57
+ {
58
+ "run": packet.run,
59
+ "instructions": pipeline.instructions(),
60
+ "packet": packet.data,
61
+ },
62
+ text,
63
+ )
64
+ return 0
65
+ raise UsageError(
66
+ "consolidation runs in your agent's session: ask it to consolidate the brain. "
67
+ "To drive it yourself, run `brain consolidate --prepare`, then `--apply OPS.json --run RUN`"
68
+ )
69
+
70
+
71
+ def review(args: Any) -> int:
72
+ from brainwiki.consolidate import list_proposals, load_proposal, pipeline
73
+
74
+ with open_engine(args.brain, host=getattr(args, "as_host", None)) as engine:
75
+ if args.action == "list":
76
+ items = list_proposals(engine)
77
+ text = "\n".join(
78
+ f"{p['run']} {p.get('agent') or ''} {p.get('created') or ''}" for p in items
79
+ )
80
+ emit(args, items, text or "No proposals.")
81
+ return 0
82
+ if not args.run:
83
+ raise UsageError(f"brain review {args.action} needs a run id")
84
+ from brainwiki.write import edits
85
+
86
+ if edits.is_edit_proposal(engine, args.run):
87
+ if args.action == "show":
88
+ text = edits.show_proposal(engine, args.run)
89
+ emit(args, {"run": args.run, "type": "edit-proposal"}, text)
90
+ return 0
91
+ if args.action == "accept":
92
+ record = edits.accept_proposal(engine, args.run)
93
+ emit(args, record.to_json(), f"Accepted {args.run} as {record.run}.")
94
+ return 0
95
+ record = edits.reject_proposal(engine, args.run)
96
+ emit(args, record.to_json(), f"Rejected {args.run}; the note is unchanged.")
97
+ return 0
98
+ if args.action == "show":
99
+ _, ops, agent, text = load_proposal(engine, args.run)
100
+ emit(args, {"run": args.run, "agent": agent, "ops": ops}, text)
101
+ return 0
102
+ if args.action == "accept":
103
+ result = pipeline.accept_review(engine, args.run, engine.profile)
104
+ emit(args, result.to_json(), result.format_text())
105
+ return 0 if result.status == "applied" else 1
106
+ record = pipeline.reject_review(engine, args.run)
107
+ emit(args, record.to_json(), f"Rejected {args.run}; its episodes stay pending.")
108
+ return 0
@@ -0,0 +1,155 @@
1
+ """version, init, list, brains, doctor, setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from brainwiki.cli._output import emit
10
+ from brainwiki.config import (
11
+ load_user_config,
12
+ open_brain,
13
+ register_brain,
14
+ set_value,
15
+ unregister_brain,
16
+ )
17
+ from brainwiki.errors import Conflict, NotFound, UsageError
18
+
19
+
20
+ def version(args: Any) -> int:
21
+ from brainwiki import FORMAT_VERSION, __version__
22
+
23
+ emit(
24
+ args,
25
+ {"version": __version__, "format": FORMAT_VERSION},
26
+ f"brain-wiki {__version__} (format {FORMAT_VERSION})",
27
+ )
28
+ return 0
29
+
30
+
31
+ def init(args: Any) -> int:
32
+ from brainwiki.config import registration_conflict
33
+ from brainwiki.lifecycle import init_brain, init_git
34
+
35
+ target = Path(args.path).expanduser()
36
+ if not args.no_register:
37
+ problem = registration_conflict(args.name or target.resolve().name, target)
38
+ if problem:
39
+ raise Conflict(problem)
40
+ root = init_brain(target, args.name)
41
+ brain = open_brain(root)
42
+ name = args.name or brain.manifest.name or root.name
43
+ if not args.no_register:
44
+ register_brain(name, root)
45
+ git_problem = init_git(root, name) if args.git else None
46
+ if not args.git:
47
+ git_line = "\n tip: `brain init --git` or `git init` to track changes"
48
+ elif git_problem:
49
+ git_line = f"\n git: {git_problem}; commit the brain's files yourself"
50
+ else:
51
+ git_line = "\n git: initialized, with a first commit"
52
+ emit(
53
+ args,
54
+ {
55
+ "path": str(root),
56
+ "name": name,
57
+ "id": brain.id,
58
+ "registered": not args.no_register,
59
+ "git": bool(args.git) and git_problem is None,
60
+ },
61
+ f"Created brain {name!r} at {root}\n id: {brain.id}"
62
+ + ("" if args.no_register else "\n registered in the user config")
63
+ + git_line,
64
+ )
65
+ return 0
66
+
67
+
68
+ def list_(args: Any) -> int:
69
+ from brainwiki.index import format_brain_list, list_brains
70
+
71
+ host = getattr(args, "as_host", None)
72
+ result = list_brains(host, explicit=args.brain)
73
+ emit(args, result, format_brain_list(result, owner=host is None))
74
+ return 0
75
+
76
+
77
+ def brains(args: Any) -> int:
78
+ user = load_user_config()
79
+ action, rest = args.action, args.args
80
+ if action == "list":
81
+ return list_(args)
82
+ if action == "add":
83
+ if len(rest) != 2:
84
+ raise UsageError("brain brains add NAME PATH")
85
+ brain = open_brain(Path(rest[1]))
86
+ register_brain(rest[0], brain.root, make_default=None)
87
+ emit(
88
+ args,
89
+ {"name": rest[0], "path": str(brain.root)},
90
+ f"Registered {rest[0]!r} -> {brain.root}",
91
+ )
92
+ return 0
93
+ if action == "remove":
94
+ if len(rest) != 1:
95
+ raise UsageError("brain brains remove NAME")
96
+ path = user.brains.get(rest[0])
97
+ if not unregister_brain(rest[0]):
98
+ raise NotFound(f"no brain named {rest[0]!r}")
99
+ if args.purge_state and path and (path / "brain.toml").exists():
100
+ from brainwiki.config import cache_dir, state_dir
101
+
102
+ brain_id = open_brain(path).id
103
+ for base in (state_dir(), cache_dir()):
104
+ shutil.rmtree(base / "brains" / brain_id, ignore_errors=True)
105
+ emit(
106
+ args,
107
+ {"removed": rest[0]},
108
+ f"Removed {rest[0]!r} from the user config (the brain folder is untouched)",
109
+ )
110
+ return 0
111
+ if action == "rename":
112
+ if len(rest) != 2 or rest[0] not in user.brains:
113
+ raise UsageError("brain brains rename OLD NEW")
114
+ path = user.brains[rest[0]]
115
+ was_default = user.default == rest[0]
116
+ unregister_brain(rest[0])
117
+ register_brain(rest[1], path, make_default=was_default)
118
+ emit(args, {"renamed": rest}, f"Renamed {rest[0]!r} to {rest[1]!r}")
119
+ return 0
120
+ if action == "default":
121
+ if len(rest) != 1 or rest[0] not in user.brains:
122
+ raise UsageError("brain brains default NAME (a registered brain)")
123
+ set_value("default", rest[0])
124
+ emit(args, {"default": rest[0]}, f"Default brain is now {rest[0]!r}")
125
+ return 0
126
+ raise UsageError(f"unknown action {action}")
127
+
128
+
129
+ def doctor(args: Any) -> int:
130
+ from brainwiki.doctor import run_doctor
131
+
132
+ report = run_doctor()
133
+ lines = [f"brain-wiki {report['version']} on {report['device']}"]
134
+ for b in report["brains"]:
135
+ mark = "ok" if b.get("ok") else "PROBLEM"
136
+ lines.append(
137
+ f" brain {b['name']}: {b['path']} [{mark}] notes={b.get('notes', '?')} pending={b.get('pending_episodes', '?')}"
138
+ )
139
+ for h in report["hosts"]:
140
+ if h.get("installed"):
141
+ lines.append(
142
+ f" host {h['id']}: installed, {'connected' if h.get('connected') else 'not connected'}"
143
+ )
144
+ sq = report["sqlite"]
145
+ lines.append(f" sqlite {sq['version']} fts5={sq['fts5']} trigram={sq['trigram']}")
146
+ for p in report["problems"]:
147
+ lines.append(f" ! {p['message']}" + (f" (fix: {p['fix']})" if p.get("fix") else ""))
148
+ emit(args, report, "\n".join(lines))
149
+ return 1 if any(p.get("severity") == "error" for p in report["problems"]) else 0
150
+
151
+
152
+ def setup(args: Any) -> int:
153
+ from brainwiki.setup_wizard import run_wizard
154
+
155
+ return run_wizard(args)