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.
- brain_wiki-0.1.0.dev0.dist-info/METADATA +231 -0
- brain_wiki-0.1.0.dev0.dist-info/RECORD +141 -0
- brain_wiki-0.1.0.dev0.dist-info/WHEEL +4 -0
- brain_wiki-0.1.0.dev0.dist-info/entry_points.txt +4 -0
- brain_wiki-0.1.0.dev0.dist-info/licenses/LICENSE +202 -0
- brainwiki/__init__.py +17 -0
- brainwiki/__main__.py +5 -0
- brainwiki/assets/plugin/.claude-plugin/marketplace.json +16 -0
- brainwiki/assets/plugin/.claude-plugin/plugin.json +19 -0
- brainwiki/assets/plugin/.mcp.json +16 -0
- brainwiki/assets/plugin/mcp.json +17 -0
- brainwiki/assets/plugin/mcp_config.json +15 -0
- brainwiki/assets/plugin/plugin.json +37 -0
- brainwiki/assets/plugin/rules/brain-wiki.md +30 -0
- brainwiki/assets/plugin/server.json +30 -0
- brainwiki/assets/plugin/skills/brain/SKILL.md +145 -0
- brainwiki/assets/plugin/skills/brain/references/format.md +149 -0
- brainwiki/assets/plugin/skills/brain-maintain/SKILL.md +111 -0
- brainwiki/assets/plugin/skills/brain-maintain/references/consolidate.md +91 -0
- brainwiki/assets/plugin/skills/brain-maintain/references/harvest.md +144 -0
- brainwiki/assets/plugin/skills/brain-setup/SKILL.md +184 -0
- brainwiki/assets/plugin/skills/brain-setup/references/hosts.md +88 -0
- brainwiki/assets/plugin/skills/brain-setup/references/privacy.md +67 -0
- brainwiki/assets/prompts/consolidate.md +89 -0
- brainwiki/assets/prompts/distill.md +96 -0
- brainwiki/assets/prompts/harvest.md +142 -0
- brainwiki/bench/__init__.py +8 -0
- brainwiki/bench/_generate.py +440 -0
- brainwiki/bench/_report.py +166 -0
- brainwiki/bench/_suites.py +959 -0
- brainwiki/cli/__init__.py +7 -0
- brainwiki/cli/_cmd_bench.py +101 -0
- brainwiki/cli/_cmd_browse.py +281 -0
- brainwiki/cli/_cmd_consolidate.py +108 -0
- brainwiki/cli/_cmd_core.py +155 -0
- brainwiki/cli/_cmd_harvest.py +137 -0
- brainwiki/cli/_cmd_hosts.py +52 -0
- brainwiki/cli/_cmd_import.py +149 -0
- brainwiki/cli/_cmd_lint.py +117 -0
- brainwiki/cli/_cmd_read.py +131 -0
- brainwiki/cli/_cmd_runs.py +66 -0
- brainwiki/cli/_cmd_viz.py +57 -0
- brainwiki/cli/_cmd_write.py +122 -0
- brainwiki/cli/_main.py +511 -0
- brainwiki/cli/_output.py +19 -0
- brainwiki/config/__init__.py +56 -0
- brainwiki/config/_dirs.py +69 -0
- brainwiki/config/_manifest.py +307 -0
- brainwiki/config/_resolve.py +169 -0
- brainwiki/config/_user.py +183 -0
- brainwiki/consolidate/__init__.py +19 -0
- brainwiki/consolidate/_apply.py +408 -0
- brainwiki/consolidate/_packet.py +489 -0
- brainwiki/consolidate/_pipeline.py +215 -0
- brainwiki/consolidate/_presets.py +230 -0
- brainwiki/consolidate/_review.py +104 -0
- brainwiki/consolidate/_schema.py +153 -0
- brainwiki/consolidate/_validate.py +252 -0
- brainwiki/doctor.py +216 -0
- brainwiki/engine.py +80 -0
- brainwiki/errors.py +81 -0
- brainwiki/format/__init__.py +112 -0
- brainwiki/format/_agents_md.py +26 -0
- brainwiki/format/_claims.py +95 -0
- brainwiki/format/_document.py +203 -0
- brainwiki/format/_edit.py +94 -0
- brainwiki/format/_episode.py +67 -0
- brainwiki/format/_fields.py +350 -0
- brainwiki/format/_frontmatter.py +175 -0
- brainwiki/format/_links.py +72 -0
- brainwiki/format/_meta.py +67 -0
- brainwiki/format/_normalize.py +70 -0
- brainwiki/format/_relations.py +211 -0
- brainwiki/format/_rules.py +41 -0
- brainwiki/harvest.py +414 -0
- brainwiki/hosts/__init__.py +22 -0
- brainwiki/hosts/_antigravity.py +163 -0
- brainwiki/hosts/_claude_code.py +174 -0
- brainwiki/hosts/_codex.py +149 -0
- brainwiki/hosts/_connect.py +443 -0
- brainwiki/hosts/_detect.py +39 -0
- brainwiki/hosts/_instructions.py +220 -0
- brainwiki/hosts/_sessions.py +176 -0
- brainwiki/ids.py +69 -0
- brainwiki/importer/__init__.py +17 -0
- brainwiki/importer/_episodes.py +112 -0
- brainwiki/importer/_into.py +102 -0
- brainwiki/importer/_normalize.py +64 -0
- brainwiki/importer/_overlay.py +199 -0
- brainwiki/importer/_survey.py +167 -0
- brainwiki/index/__init__.py +44 -0
- brainwiki/index/_brains.py +119 -0
- brainwiki/index/_db.py +131 -0
- brainwiki/index/_guards.py +154 -0
- brainwiki/index/_neighbors.py +174 -0
- brainwiki/index/_orient.py +400 -0
- brainwiki/index/_read.py +285 -0
- brainwiki/index/_resolver.py +147 -0
- brainwiki/index/_scanner.py +665 -0
- brainwiki/index/_schema.py +164 -0
- brainwiki/index/_search.py +659 -0
- brainwiki/index/_topics.py +253 -0
- brainwiki/index/_written.py +29 -0
- brainwiki/lifecycle.py +158 -0
- brainwiki/lint/__init__.py +20 -0
- brainwiki/lint/_catalog.py +85 -0
- brainwiki/lint/_cross.py +649 -0
- brainwiki/lint/_files.py +483 -0
- brainwiki/lint/_fixes.py +87 -0
- brainwiki/lint/_hook.py +54 -0
- brainwiki/lint/_model.py +103 -0
- brainwiki/lint/_reports.py +242 -0
- brainwiki/lint/_runner.py +156 -0
- brainwiki/logs.py +34 -0
- brainwiki/mcp/__init__.py +7 -0
- brainwiki/mcp/_server.py +917 -0
- brainwiki/origin.py +97 -0
- brainwiki/runs/__init__.py +21 -0
- brainwiki/runs/_git.py +89 -0
- brainwiki/runs/_lock.py +42 -0
- brainwiki/runs/_record.py +145 -0
- brainwiki/runs/_transaction.py +441 -0
- brainwiki/runs/_undo.py +159 -0
- brainwiki/setup_wizard.py +143 -0
- brainwiki/store/__init__.py +7 -0
- brainwiki/store/_local.py +153 -0
- brainwiki/viz/__init__.py +9 -0
- brainwiki/viz/_export.py +414 -0
- brainwiki/viz/_lod.py +455 -0
- brainwiki/viz/_server.py +327 -0
- brainwiki/viz/app/BUILD_INFO.json +5 -0
- brainwiki/viz/app/THIRD_PARTY_NOTICES.txt +1519 -0
- brainwiki/viz/app/assets/index-gK1pnQdO.js +4496 -0
- brainwiki/viz/app/assets/style-Cg8NUCst.css +1 -0
- brainwiki/viz/app/index.html +14 -0
- brainwiki/write/__init__.py +23 -0
- brainwiki/write/_edits.py +730 -0
- brainwiki/write/_episode.py +195 -0
- brainwiki/write/_move.py +181 -0
- brainwiki/write/_rules.py +204 -0
- brainwiki/write/_secrets.py +108 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""harvest, origin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from brainwiki.cli._output import emit, warn
|
|
10
|
+
from brainwiki.errors import UsageError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def harvest(args: Any) -> int:
|
|
14
|
+
from brainwiki import harvest as hv
|
|
15
|
+
from brainwiki.config import load_user_config
|
|
16
|
+
|
|
17
|
+
user = load_user_config()
|
|
18
|
+
hosts = args.hosts or list(user.harvest.get("hosts") or ["claude-code", "codex", "antigravity"])
|
|
19
|
+
candidates = hv.discover(user, hosts, since=args.since, project=args.project)
|
|
20
|
+
eligible = [c for c in candidates if not c.skip]
|
|
21
|
+
if args.status:
|
|
22
|
+
rows = []
|
|
23
|
+
for host in hosts:
|
|
24
|
+
mine = [c for c in candidates if c.info.host == host]
|
|
25
|
+
rows.append(
|
|
26
|
+
{
|
|
27
|
+
"host": host,
|
|
28
|
+
"enabled": user.harvest_enabled(host),
|
|
29
|
+
"sessions": len(mine),
|
|
30
|
+
"unharvested": sum(1 for c in mine if not c.skip),
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
text = "\n".join(
|
|
34
|
+
f"{r['host']}: {'on' if r['enabled'] else 'off'}, {r['unharvested']} of {r['sessions']} sessions to harvest"
|
|
35
|
+
for r in rows
|
|
36
|
+
)
|
|
37
|
+
if not all(r["enabled"] for r in rows):
|
|
38
|
+
text += (
|
|
39
|
+
f"\nTo turn harvest on, add to {user.path}:\n"
|
|
40
|
+
'[harvest]\nenabled = true\nhosts = ["claude-code", "codex", "antigravity"]'
|
|
41
|
+
)
|
|
42
|
+
emit(args, rows, text)
|
|
43
|
+
return 0
|
|
44
|
+
if not args.apply:
|
|
45
|
+
data = [c.to_json() for c in candidates]
|
|
46
|
+
lines = [
|
|
47
|
+
f"{c.info.host} {c.info.id[:12]} → {c.brain.display_name if c.brain else '-'} "
|
|
48
|
+
+ (
|
|
49
|
+
f"skip: {c.skip}"
|
|
50
|
+
if c.skip
|
|
51
|
+
else f"turns {c.to_json()['turns']}, ~{c.to_json()['estimated_tokens']} tokens"
|
|
52
|
+
)
|
|
53
|
+
for c in candidates
|
|
54
|
+
]
|
|
55
|
+
total = sum(c.to_json()["estimated_tokens"] for c in eligible)
|
|
56
|
+
if any(c.skip == "routes to no brain" for c in candidates):
|
|
57
|
+
lines.append(
|
|
58
|
+
"Sessions route to the brain of their working directory: put a .brain-wiki.toml "
|
|
59
|
+
'with `brain = "NAME"` in the project, or map folders in [harvest.routes] in '
|
|
60
|
+
f"{user.path}."
|
|
61
|
+
)
|
|
62
|
+
lines.append(
|
|
63
|
+
f"{len(eligible)} sessions to harvest, ~{total} tokens to distill."
|
|
64
|
+
+ (" Run with --apply." if eligible else "")
|
|
65
|
+
)
|
|
66
|
+
emit(args, data, "\n".join(lines))
|
|
67
|
+
return 0
|
|
68
|
+
preset = None if args.raw else (args.distill or user.harvest.get("distill") or "claude-code")
|
|
69
|
+
if preset == "raw":
|
|
70
|
+
preset = None
|
|
71
|
+
results = [_harvest_one(hv, c, preset, dry_run=args.dry_run) for c in eligible]
|
|
72
|
+
written = [p for r in results for p in r["episodes"]]
|
|
73
|
+
emit(args, results, f"Wrote {len(written)} episodes from {len(eligible)} sessions.")
|
|
74
|
+
return 0 if all(not r.get("error") for r in results) else 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _harvest_one(hv: Any, candidate: Any, preset: str | None, *, dry_run: bool) -> dict[str, Any]:
|
|
78
|
+
from brainwiki.consolidate import run_command
|
|
79
|
+
|
|
80
|
+
out: dict[str, Any] = {
|
|
81
|
+
"session": candidate.info.id,
|
|
82
|
+
"host": candidate.info.host,
|
|
83
|
+
"episodes": [],
|
|
84
|
+
}
|
|
85
|
+
prompt_head = (resources.files("brainwiki") / "assets" / "prompts" / "distill.md").read_text(
|
|
86
|
+
encoding="utf-8"
|
|
87
|
+
)
|
|
88
|
+
for group in hv.chunks(candidate.new_turns):
|
|
89
|
+
first, last = group[0].index, group[-1].index
|
|
90
|
+
if dry_run:
|
|
91
|
+
out["episodes"].append(f"(dry run) turns {first}-{last}")
|
|
92
|
+
continue
|
|
93
|
+
if preset is None:
|
|
94
|
+
out["episodes"].append(
|
|
95
|
+
hv.write_episode_for(candidate, first, last, None, raw_turns=group)
|
|
96
|
+
)
|
|
97
|
+
hv.advance(candidate.info.host, candidate.info.id, last)
|
|
98
|
+
continue
|
|
99
|
+
turns, truncated = hv.condense(group, hv.CHUNK_CHARS)
|
|
100
|
+
payload = json.dumps(hv.session_payload(candidate, turns, truncated), ensure_ascii=False)
|
|
101
|
+
try:
|
|
102
|
+
reply = run_command(preset, prompt_head + "\n" + payload + "\n")
|
|
103
|
+
except Exception as exc: # noqa: BLE001 - report per session and continue
|
|
104
|
+
out["error"] = str(exc)
|
|
105
|
+
warn(f"{candidate.info.id}: {exc}")
|
|
106
|
+
return out
|
|
107
|
+
data = _first_json(reply)
|
|
108
|
+
distilled, problems = hv.parse_distilled(data, first, last)
|
|
109
|
+
if problems:
|
|
110
|
+
out["error"] = "; ".join(problems)
|
|
111
|
+
return out
|
|
112
|
+
if distilled is not None and not distilled.skip:
|
|
113
|
+
out["episodes"].append(hv.write_episode_for(candidate, first, last, distilled))
|
|
114
|
+
hv.advance(candidate.info.host, candidate.info.id, last)
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _first_json(text: str) -> Any:
|
|
119
|
+
decoder = json.JSONDecoder()
|
|
120
|
+
for start in [i for i, ch in enumerate(text) if ch == "{"]:
|
|
121
|
+
try:
|
|
122
|
+
value, _ = decoder.raw_decode(text[start:])
|
|
123
|
+
except ValueError:
|
|
124
|
+
continue
|
|
125
|
+
if isinstance(value, dict):
|
|
126
|
+
return value
|
|
127
|
+
raise UsageError("the distill reply contained no JSON object")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def origin(args: Any) -> int:
|
|
131
|
+
from brainwiki.engine import open_engine
|
|
132
|
+
from brainwiki.origin import format_origin, origin_window
|
|
133
|
+
|
|
134
|
+
with open_engine(args.brain, host=getattr(args, "as_host", None)) as engine:
|
|
135
|
+
result = origin_window(engine, args.episode, turns=args.turns)
|
|
136
|
+
emit(args, result, format_origin(result))
|
|
137
|
+
return 0
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""connect, mcp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def mcp(args: Any) -> int:
|
|
9
|
+
from brainwiki.mcp import serve
|
|
10
|
+
|
|
11
|
+
serve(args.host, args.brain)
|
|
12
|
+
return 0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def connect(args: Any) -> int:
|
|
16
|
+
from brainwiki.cli._output import emit
|
|
17
|
+
from brainwiki.errors import UsageError
|
|
18
|
+
from brainwiki.hosts import connect as cn
|
|
19
|
+
|
|
20
|
+
if args.uninstall:
|
|
21
|
+
steps = cn.uninstall(args.host, dry_run=args.dry_run)
|
|
22
|
+
emit(args, {"host": args.host, "steps": steps}, "\n".join(steps))
|
|
23
|
+
return 0
|
|
24
|
+
if args.print_only or args.host == "generic":
|
|
25
|
+
if args.host not in cn.HOSTS:
|
|
26
|
+
raise UsageError(f"unknown host {args.host!r}; use one of {', '.join(cn.HOSTS)}")
|
|
27
|
+
text = cn.print_text(args.host, args.brain)
|
|
28
|
+
emit(args, {"text": text}, text)
|
|
29
|
+
return 0
|
|
30
|
+
plan = cn.plan(args.host, scope=args.scope, brain_name=args.brain, force=args.force)
|
|
31
|
+
if args.dry_run:
|
|
32
|
+
emit(args, {"plan": plan.describe()}, plan.describe())
|
|
33
|
+
return 0
|
|
34
|
+
record = cn.apply(plan, brain_name=args.brain, scope=args.scope)
|
|
35
|
+
failed = [c for c in record["commands"] if not c["ok"]]
|
|
36
|
+
lines = [f"Connected {args.host}: plugin at {record['plugin_dir']}"]
|
|
37
|
+
lines += [
|
|
38
|
+
f" instructions: {item['written']}"
|
|
39
|
+
for item in record.get("instructions", [])
|
|
40
|
+
if not item.get("plugin")
|
|
41
|
+
]
|
|
42
|
+
lines += [f" failed: {' '.join(c['run'])}: {c['output'].strip()}" for c in failed]
|
|
43
|
+
lines += plan.notes
|
|
44
|
+
if "visible" in record:
|
|
45
|
+
lines.append(
|
|
46
|
+
" verified: brain-wiki is listed"
|
|
47
|
+
if record["visible"]
|
|
48
|
+
else " warning: brain-wiki is not listed yet"
|
|
49
|
+
)
|
|
50
|
+
lines.append(f"Undo with: brain connect {args.host} --uninstall")
|
|
51
|
+
emit(args, record, "\n".join(lines))
|
|
52
|
+
return 1 if failed else 0
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""import."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from brainwiki.cli._output import emit
|
|
9
|
+
from brainwiki.errors import NotFound, UsageError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def import_(args: Any) -> int:
|
|
13
|
+
if args.from_host:
|
|
14
|
+
return _from_host(args)
|
|
15
|
+
if not args.path:
|
|
16
|
+
raise UsageError("give a folder or file to import, or --from HOST")
|
|
17
|
+
source = Path(args.path).expanduser()
|
|
18
|
+
if args.overlay:
|
|
19
|
+
return _overlay(args, source)
|
|
20
|
+
if args.into:
|
|
21
|
+
return _into(args, source)
|
|
22
|
+
if args.to_episodes:
|
|
23
|
+
return _to_episodes(args, source)
|
|
24
|
+
raise UsageError("choose a mode: --overlay, --into BRAIN, --to-episodes, or --from HOST")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _overlay(args: Any, source: Path) -> int:
|
|
28
|
+
from brainwiki.importer import overlay
|
|
29
|
+
|
|
30
|
+
if args.phase == 0:
|
|
31
|
+
if not args.apply:
|
|
32
|
+
from brainwiki.importer import survey
|
|
33
|
+
|
|
34
|
+
found = survey(source)
|
|
35
|
+
text = overlay.report(found, args.brain or source.resolve().name)
|
|
36
|
+
emit(
|
|
37
|
+
args,
|
|
38
|
+
found.to_json(),
|
|
39
|
+
text + "\nNothing changed. Run with --apply to adopt (phase 0).",
|
|
40
|
+
)
|
|
41
|
+
return 0
|
|
42
|
+
result = overlay.adopt(source, name=args.brain)
|
|
43
|
+
emit(
|
|
44
|
+
args,
|
|
45
|
+
result,
|
|
46
|
+
f"Adopted {result['root']} as brain {result['brain']!r}. Next: --phase 1, 2, 3 (each optional).",
|
|
47
|
+
)
|
|
48
|
+
return 0
|
|
49
|
+
from brainwiki.config import open_brain
|
|
50
|
+
from brainwiki.engine import Engine
|
|
51
|
+
|
|
52
|
+
with Engine(open_brain(source)) as engine:
|
|
53
|
+
record, diff = overlay.apply_phase(engine, args.phase, dry_run=not args.apply)
|
|
54
|
+
if not args.apply:
|
|
55
|
+
emit(
|
|
56
|
+
args,
|
|
57
|
+
{"diff": diff},
|
|
58
|
+
(diff or "Nothing to change.") + ("\n\nRun with --apply." if diff else ""),
|
|
59
|
+
)
|
|
60
|
+
return 0
|
|
61
|
+
if record is None:
|
|
62
|
+
emit(args, {"run": None}, "Nothing to change.")
|
|
63
|
+
return 0
|
|
64
|
+
emit(
|
|
65
|
+
args,
|
|
66
|
+
record.to_json(),
|
|
67
|
+
f"Phase {args.phase} applied ({record.run}); undo with brain undo {record.run}",
|
|
68
|
+
)
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _into(args: Any, source: Path) -> int:
|
|
73
|
+
from brainwiki.engine import open_engine
|
|
74
|
+
from brainwiki.importer import plan_into, tidy_after_import
|
|
75
|
+
from brainwiki.lint import run_gate
|
|
76
|
+
|
|
77
|
+
with open_engine(args.into) as engine:
|
|
78
|
+
run, summary = plan_into(engine, source, topic=args.topic)
|
|
79
|
+
if not args.apply:
|
|
80
|
+
lines = [
|
|
81
|
+
f"Would copy {summary['files']} files into topics {', '.join(summary['topics'])}."
|
|
82
|
+
]
|
|
83
|
+
lines.append(
|
|
84
|
+
f"Markdown links to files outside the source: {summary['unresolved_md_links']}"
|
|
85
|
+
)
|
|
86
|
+
emit(args, summary, "\n".join(lines) + "\nNothing changed. Run with --apply.")
|
|
87
|
+
return 0
|
|
88
|
+
record = run.commit(
|
|
89
|
+
check=run_gate(engine), log_lines=[f"- imported {summary['files']} files from {source}"]
|
|
90
|
+
)
|
|
91
|
+
tidy = [r.run for r in tidy_after_import(engine)]
|
|
92
|
+
text = f"Imported {summary['files']} files ({record.run})."
|
|
93
|
+
if tidy:
|
|
94
|
+
text += f" Then added hubs and refreshed AGENTS.md ({', '.join(tidy)}); undo all to revert."
|
|
95
|
+
emit(args, record.to_json() | summary | {"tidy_runs": tidy}, text)
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _to_episodes(args: Any, source: Path) -> int:
|
|
100
|
+
from brainwiki.engine import open_engine
|
|
101
|
+
from brainwiki.importer import import_documents
|
|
102
|
+
|
|
103
|
+
if not source.exists():
|
|
104
|
+
raise NotFound(f"no such file or folder: {source}")
|
|
105
|
+
paths = sorted(source.rglob("*.md")) if source.is_dir() else [source]
|
|
106
|
+
with open_engine(args.brain, host=getattr(args, "as_host", None)) as engine:
|
|
107
|
+
results = import_documents(
|
|
108
|
+
engine,
|
|
109
|
+
paths,
|
|
110
|
+
base=source if source.is_dir() else source.parent,
|
|
111
|
+
origin="import",
|
|
112
|
+
inputs="external" if args.untrusted else "local",
|
|
113
|
+
agent=args.agent,
|
|
114
|
+
topic=args.topic,
|
|
115
|
+
dry_run=not args.apply,
|
|
116
|
+
)
|
|
117
|
+
failed = [r["file"] for r in results if r.get("error")]
|
|
118
|
+
written = len(results) - len(failed)
|
|
119
|
+
verb = "Would write" if not args.apply else "Wrote"
|
|
120
|
+
text = f"{verb} {written} episodes from {len(paths) - len(failed)} files."
|
|
121
|
+
if failed:
|
|
122
|
+
text += f" Could not read {len(failed)}: " + ", ".join(failed)
|
|
123
|
+
emit(args, results, text + ("" if args.apply else " Run with --apply."))
|
|
124
|
+
return 1 if failed else 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _from_host(args: Any) -> int:
|
|
128
|
+
from brainwiki.engine import open_engine
|
|
129
|
+
from brainwiki.importer import import_documents, memory_files
|
|
130
|
+
|
|
131
|
+
override = Path(args.path).expanduser() if args.path else None
|
|
132
|
+
files = memory_files(args.from_host, override)
|
|
133
|
+
with open_engine(args.brain) as engine:
|
|
134
|
+
results = import_documents(
|
|
135
|
+
engine,
|
|
136
|
+
files,
|
|
137
|
+
base=Path.home(),
|
|
138
|
+
origin="agent",
|
|
139
|
+
inputs="unknown",
|
|
140
|
+
agent=args.from_host,
|
|
141
|
+
topic=args.topic,
|
|
142
|
+
dry_run=not args.apply,
|
|
143
|
+
)
|
|
144
|
+
lines = [f"{args.from_host}: {len(files)} memory files, {len(results)} episodes"]
|
|
145
|
+
lines += [f" {f}" for f in files]
|
|
146
|
+
if not args.apply:
|
|
147
|
+
lines.append("Nothing changed. Run with --apply.")
|
|
148
|
+
emit(args, results, "\n".join(lines))
|
|
149
|
+
return 0
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""lint, report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from brainwiki.cli._output import emit
|
|
10
|
+
from brainwiki.engine import Engine, open_engine
|
|
11
|
+
from brainwiki.errors import NotFound
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _engine(args: Any) -> Engine:
|
|
15
|
+
return open_engine(args.brain, host=getattr(args, "as_host", None))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _codes(raw: str | None) -> list[str] | None:
|
|
19
|
+
return [c.strip().upper() for c in raw.split(",") if c.strip()] if raw else None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def brain_paths(engine: Engine, raw: list[str]) -> list[str] | None:
|
|
23
|
+
"""CLI paths (absolute, cwd-relative, or brain-relative) as brain-relative paths."""
|
|
24
|
+
if not raw:
|
|
25
|
+
return None
|
|
26
|
+
out = []
|
|
27
|
+
for item in raw:
|
|
28
|
+
candidate = Path(item) if Path(item).is_absolute() else Path.cwd() / item
|
|
29
|
+
try:
|
|
30
|
+
rel = candidate.resolve().relative_to(engine.brain.root).as_posix()
|
|
31
|
+
except ValueError:
|
|
32
|
+
rel = item.replace("\\", "/").strip("/")
|
|
33
|
+
out.append("" if rel == "." else rel)
|
|
34
|
+
return out
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _changed_paths(engine: Engine) -> list[str]:
|
|
38
|
+
row = engine.db.execute("SELECT value FROM meta WHERE key='last_lint_ns'").fetchone()
|
|
39
|
+
since = int(row["value"]) if row else 0
|
|
40
|
+
return [
|
|
41
|
+
r["path"] for r in engine.db.execute("SELECT path FROM files WHERE mtime_ns > ?", (since,))
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _mark_linted(engine: Engine) -> None:
|
|
46
|
+
engine.db.execute(
|
|
47
|
+
"INSERT OR REPLACE INTO meta(key, value) VALUES ('last_lint_ns', ?)", (str(time.time_ns()),)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def lint(args: Any) -> int:
|
|
52
|
+
from brainwiki.lint import lint as run_lint
|
|
53
|
+
|
|
54
|
+
with _engine(args) as engine:
|
|
55
|
+
if args.install_hook:
|
|
56
|
+
from brainwiki.lint import install_hook
|
|
57
|
+
|
|
58
|
+
target = install_hook(engine.brain.root)
|
|
59
|
+
emit(args, {"installed": str(target)}, f"Installed {target}")
|
|
60
|
+
return 0
|
|
61
|
+
paths = brain_paths(engine, args.paths)
|
|
62
|
+
for raw, rel in zip(args.paths or [], paths or [], strict=True):
|
|
63
|
+
if not (engine.brain.root / rel).exists():
|
|
64
|
+
raise NotFound(f"no such file or folder in the brain: {raw}")
|
|
65
|
+
select, ignore = _codes(args.select), _codes(args.ignore)
|
|
66
|
+
if args.fix:
|
|
67
|
+
from brainwiki.lint import fix
|
|
68
|
+
|
|
69
|
+
outcome = fix(
|
|
70
|
+
engine,
|
|
71
|
+
paths=paths,
|
|
72
|
+
profile=args.profile,
|
|
73
|
+
select=select,
|
|
74
|
+
ignore=ignore,
|
|
75
|
+
dry_run=args.dry_run,
|
|
76
|
+
)
|
|
77
|
+
if args.dry_run:
|
|
78
|
+
emit(
|
|
79
|
+
args,
|
|
80
|
+
{"fixes": [p.to_json() for p in outcome.applied], "diff": outcome.diff},
|
|
81
|
+
outcome.diff or "Nothing to fix.",
|
|
82
|
+
)
|
|
83
|
+
return 0
|
|
84
|
+
data = outcome.remaining.to_json()
|
|
85
|
+
data["fixed"] = [p.to_json() for p in outcome.applied]
|
|
86
|
+
data["run"] = outcome.record.run if outcome.record else None
|
|
87
|
+
text = (
|
|
88
|
+
f"Fixed {len(outcome.applied)} problem(s) in {outcome.record.run} (undo: brain undo {outcome.record.run})\n"
|
|
89
|
+
if outcome.record
|
|
90
|
+
else "Nothing to fix.\n"
|
|
91
|
+
) + outcome.remaining.format_text()
|
|
92
|
+
emit(args, data, text)
|
|
93
|
+
return 1 if outcome.remaining.errors else 0
|
|
94
|
+
staged = None
|
|
95
|
+
if args.staged:
|
|
96
|
+
from brainwiki.lint import staged_files
|
|
97
|
+
|
|
98
|
+
staged = staged_files(engine.brain.root)
|
|
99
|
+
elif args.changed:
|
|
100
|
+
engine.scan()
|
|
101
|
+
paths = _changed_paths(engine)
|
|
102
|
+
result = run_lint(
|
|
103
|
+
engine, paths=paths, profile=args.profile, select=select, ignore=ignore, staged=staged
|
|
104
|
+
)
|
|
105
|
+
if staged is None and (args.changed or paths is None):
|
|
106
|
+
_mark_linted(engine)
|
|
107
|
+
emit(args, result.to_json(), result.format_text())
|
|
108
|
+
return 1 if result.errors else 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def report(args: Any) -> int:
|
|
112
|
+
from brainwiki.lint import generate
|
|
113
|
+
|
|
114
|
+
with _engine(args) as engine:
|
|
115
|
+
written = generate(engine)
|
|
116
|
+
emit(args, {"written": written}, "\n".join(f"wrote {p}" for p in written))
|
|
117
|
+
return 0
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""orient, search, read, neighbors, index."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from brainwiki.cli._output import emit
|
|
8
|
+
from brainwiki.engine import open_engine
|
|
9
|
+
from brainwiki.errors import UsageError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _engine(args: Any):
|
|
13
|
+
return open_engine(args.brain, host=getattr(args, "as_host", None))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def orient(args: Any) -> int:
|
|
17
|
+
from brainwiki.index import orient as run
|
|
18
|
+
|
|
19
|
+
with _engine(args) as engine:
|
|
20
|
+
result = run(engine)
|
|
21
|
+
emit(args, result, result["text"])
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def search(args: Any) -> int:
|
|
26
|
+
from brainwiki.index import format_search_text
|
|
27
|
+
from brainwiki.index import search as run
|
|
28
|
+
|
|
29
|
+
include = {part.strip() for part in (args.include or "").split(",") if part.strip()}
|
|
30
|
+
with _engine(args) as engine:
|
|
31
|
+
result = run(
|
|
32
|
+
engine,
|
|
33
|
+
args.query,
|
|
34
|
+
limit=args.limit,
|
|
35
|
+
topics=args.topic,
|
|
36
|
+
types=args.types,
|
|
37
|
+
tags=args.tags,
|
|
38
|
+
where=_parse_where(args.where),
|
|
39
|
+
since=args.since,
|
|
40
|
+
until=args.until,
|
|
41
|
+
sort=args.sort,
|
|
42
|
+
cursor=args.cursor,
|
|
43
|
+
count_only=args.count,
|
|
44
|
+
facets=args.facets,
|
|
45
|
+
intent=args.intent,
|
|
46
|
+
include_episodes=True
|
|
47
|
+
if "episodes" in include
|
|
48
|
+
else (False if args.no_episodes else None),
|
|
49
|
+
include_archived="archived" in include,
|
|
50
|
+
include_archived_episodes="archived_episodes" in include,
|
|
51
|
+
)
|
|
52
|
+
if args.count and not args.json and not args.facets:
|
|
53
|
+
print(result["total"])
|
|
54
|
+
return 0
|
|
55
|
+
emit(args, result, format_search_text(result, "paths" if args.paths else "text"))
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_where(items: list[str]) -> dict[str, str]:
|
|
60
|
+
where = {}
|
|
61
|
+
for item in items or []:
|
|
62
|
+
key, sep, value = item.partition("=")
|
|
63
|
+
if not sep or not key.strip():
|
|
64
|
+
raise UsageError(f"--where takes KEY=VALUE, not {item!r}")
|
|
65
|
+
where[key.strip()] = value.strip()
|
|
66
|
+
return where
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def read(args: Any) -> int:
|
|
70
|
+
from brainwiki.index import format_read_text
|
|
71
|
+
from brainwiki.index import read as run
|
|
72
|
+
|
|
73
|
+
with _engine(args) as engine:
|
|
74
|
+
result = run(engine, args.target, mode=args.mode, section=args.section)
|
|
75
|
+
emit(args, result, format_read_text(result))
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def neighbors(args: Any) -> int:
|
|
80
|
+
from brainwiki.index import format_neighbors_text
|
|
81
|
+
from brainwiki.index import neighbors as run
|
|
82
|
+
|
|
83
|
+
with _engine(args) as engine:
|
|
84
|
+
result = run(
|
|
85
|
+
engine,
|
|
86
|
+
args.target,
|
|
87
|
+
relations=args.relations,
|
|
88
|
+
direction=args.direction,
|
|
89
|
+
depth=args.depth,
|
|
90
|
+
limit=args.limit,
|
|
91
|
+
)
|
|
92
|
+
text = (
|
|
93
|
+
"\n".join(n["path"] for n in result["nodes"][1:])
|
|
94
|
+
if args.paths
|
|
95
|
+
else format_neighbors_text(result)
|
|
96
|
+
)
|
|
97
|
+
emit(args, result, text)
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def index(args: Any) -> int:
|
|
102
|
+
with _engine(args) as engine:
|
|
103
|
+
stats = engine.rebuild() if args.rebuild else engine.scan(force=True)
|
|
104
|
+
counts = {
|
|
105
|
+
row["kind"]: row["c"]
|
|
106
|
+
for row in engine.db.execute("SELECT kind, COUNT(*) AS c FROM files GROUP BY kind")
|
|
107
|
+
}
|
|
108
|
+
edges = engine.db.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
|
|
109
|
+
unresolved = engine.db.execute("SELECT COUNT(*) FROM edges WHERE other IS NULL").fetchone()[
|
|
110
|
+
0
|
|
111
|
+
]
|
|
112
|
+
data = {
|
|
113
|
+
"files": counts,
|
|
114
|
+
"edges": edges,
|
|
115
|
+
"unresolved_links": unresolved,
|
|
116
|
+
"scan": {
|
|
117
|
+
"added": stats.added,
|
|
118
|
+
"changed": stats.changed,
|
|
119
|
+
"removed": stats.removed,
|
|
120
|
+
"seconds": round(stats.seconds, 3),
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
text = (
|
|
124
|
+
"Index "
|
|
125
|
+
+ ("rebuilt" if args.rebuild else "up to date")
|
|
126
|
+
+ f" in {stats.seconds:.2f}s: "
|
|
127
|
+
+ ", ".join(f"{k}={v}" for k, v in sorted(counts.items()))
|
|
128
|
+
+ f"; edges={edges} (unresolved {unresolved})"
|
|
129
|
+
)
|
|
130
|
+
emit(args, data, text)
|
|
131
|
+
return 0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""runs, undo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from brainwiki.cli._output import emit
|
|
8
|
+
from brainwiki.engine import open_engine
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def runs(args: Any) -> int:
|
|
12
|
+
from brainwiki.runs import list_records, load_record
|
|
13
|
+
|
|
14
|
+
with open_engine(args.brain, host=getattr(args, "as_host", None)) as engine:
|
|
15
|
+
key = engine.brain.key
|
|
16
|
+
if args.action == "show":
|
|
17
|
+
record = load_record(key, args.run) if args.run else list_records(key)[0]
|
|
18
|
+
data = record.to_json()
|
|
19
|
+
lines = [
|
|
20
|
+
f"{record.run} {record.kind} {record.status} {record.started}",
|
|
21
|
+
f"agent: {record.agent or 'owner'}"
|
|
22
|
+
+ (f" backend: {record.backend}" if record.backend else ""),
|
|
23
|
+
]
|
|
24
|
+
if record.summary:
|
|
25
|
+
lines.append(record.summary)
|
|
26
|
+
for change in record.files:
|
|
27
|
+
if change.moved_to:
|
|
28
|
+
lines.append(f" {change.path} -> {change.moved_to}")
|
|
29
|
+
elif change.before is None:
|
|
30
|
+
lines.append(f" + {change.path}")
|
|
31
|
+
elif change.after is None:
|
|
32
|
+
lines.append(f" - {change.path}")
|
|
33
|
+
else:
|
|
34
|
+
lines.append(f" ~ {change.path}")
|
|
35
|
+
if record.git_commit:
|
|
36
|
+
lines.append(f"git: {record.git_commit[:12]}")
|
|
37
|
+
if record.git_error:
|
|
38
|
+
lines.append(f"git error: {record.git_error}")
|
|
39
|
+
if record.undone_by:
|
|
40
|
+
lines.append(f"undone by {record.undone_by}")
|
|
41
|
+
emit(args, data, "\n".join(lines))
|
|
42
|
+
return 0
|
|
43
|
+
records = list_records(key)
|
|
44
|
+
data = [
|
|
45
|
+
{
|
|
46
|
+
k: r.to_json()[k]
|
|
47
|
+
for k in ("run", "kind", "status", "started", "agent", "summary", "undone_by")
|
|
48
|
+
}
|
|
49
|
+
for r in records
|
|
50
|
+
]
|
|
51
|
+
text = "\n".join(
|
|
52
|
+
f"{r.run} {r.kind:<12} {r.status:<11} {r.started} {r.summary}"
|
|
53
|
+
+ (f" (undone by {r.undone_by})" if r.undone_by else "")
|
|
54
|
+
for r in records
|
|
55
|
+
)
|
|
56
|
+
emit(args, data, text or "No runs on this device.")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def undo(args: Any) -> int:
|
|
61
|
+
from brainwiki.runs import undo as run_undo
|
|
62
|
+
|
|
63
|
+
with open_engine(args.brain, host=getattr(args, "as_host", None)) as engine:
|
|
64
|
+
result = run_undo(engine, args.run, force=args.force, agent=getattr(args, "as_host", None))
|
|
65
|
+
emit(args, result.to_json(), f"{result.summary} ({result.run})")
|
|
66
|
+
return 0
|