polymath-nexus 0.2.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.
nexus/__init__.py ADDED
@@ -0,0 +1,104 @@
1
+ """
2
+ Brain — long-term project memory.
3
+
4
+ This is the public API surface of the brain. Anything not exported here is
5
+ internal to brain and may change without notice. The surface is intentionally
6
+ small — the brain's value is in the *file format* (markdown + JSONL + date
7
+ stamps + owner attribution), not in any specific Python API.
8
+
9
+ Long-term contract: nothing inside `nexus.*` imports anything from
10
+ `polymath.*` outside the brain. Enforced by tests/test_brain_decoupling.py.
11
+
12
+ This boundary is what makes the brain extractable into its own package
13
+ later — when that happens, this exact `__init__.py` becomes the top-level
14
+ of the new `brain` distribution.
15
+
16
+ ────────────────────────────────────────────────────────────────────────
17
+ Quick reference
18
+
19
+ # Discovery
20
+ from nexus import find_brain_root, find_repo_root, current_owner_slug
21
+
22
+ # Init
23
+ from nexus import init_brain_root
24
+
25
+ # Reading
26
+ from nexus import read_context, build_context_injection
27
+ from nexus import brain_grep, brain_usage
28
+
29
+ # Writing
30
+ from nexus import append_context, write_context
31
+
32
+ # Types
33
+ from nexus import TaskType, CONTEXT_FILES, AuthExpiredError
34
+
35
+ # Memory (chunked, embedded — optional, opt-in)
36
+ from nexus import Chunk, MemoryStore, harvest_session
37
+ """
38
+ from __future__ import annotations
39
+
40
+ # ── Discovery ────────────────────────────────────────────────────────────────
41
+ from nexus.discovery import (
42
+ current_owner_slug,
43
+ find_brain_root,
44
+ find_repo_root,
45
+ )
46
+
47
+ # ── Types (canonical home) ───────────────────────────────────────────────────
48
+ from nexus.types import (
49
+ ALWAYS_INJECT,
50
+ CONTEXT_DESCRIPTIONS,
51
+ CONTEXT_FILES,
52
+ TASK_CONTEXT_MAP,
53
+ AuthExpiredError,
54
+ TaskType,
55
+ )
56
+
57
+ # ── Config ───────────────────────────────────────────────────────────────────
58
+ from nexus.config import (
59
+ BRAIN_CACHE_DIR,
60
+ BRAIN_DIR_NAME,
61
+ BRAIN_HOME,
62
+ BRAIN_PROJECTS_DIR,
63
+ MIRROR_FILES,
64
+ )
65
+
66
+ # ── Index (read/write) ───────────────────────────────────────────────────────
67
+ from nexus.index import (
68
+ append_context,
69
+ brain_grep,
70
+ brain_usage,
71
+ build_context_injection,
72
+ context_dir,
73
+ create_project,
74
+ init_brain_root,
75
+ list_context_files,
76
+ list_projects,
77
+ read_context,
78
+ write_context,
79
+ )
80
+
81
+ # ── Memory (chunked store, optional) ─────────────────────────────────────────
82
+ from nexus.memory.store import Chunk, MemoryStore
83
+ from nexus.memory.writer import (
84
+ HarvestResult,
85
+ harvest_session,
86
+ )
87
+
88
+ __all__ = [
89
+ # Discovery
90
+ "current_owner_slug", "find_brain_root", "find_repo_root",
91
+ # Types
92
+ "TaskType", "AuthExpiredError",
93
+ "CONTEXT_FILES", "CONTEXT_DESCRIPTIONS", "TASK_CONTEXT_MAP", "ALWAYS_INJECT",
94
+ # Config
95
+ "BRAIN_HOME", "BRAIN_PROJECTS_DIR", "BRAIN_CACHE_DIR", "BRAIN_DIR_NAME",
96
+ "MIRROR_FILES",
97
+ # Index
98
+ "init_brain_root", "create_project", "list_projects",
99
+ "context_dir", "list_context_files",
100
+ "read_context", "write_context", "append_context",
101
+ "build_context_injection", "brain_grep", "brain_usage",
102
+ # Memory
103
+ "Chunk", "MemoryStore", "HarvestResult", "harvest_session",
104
+ ]
nexus/cli.py ADDED
@@ -0,0 +1,223 @@
1
+ """
2
+ `nexus` — standalone CLI for the long-term brain.
3
+
4
+ No LLM dependencies. Pure stdlib + nexus. Works with `pip install nexus`
5
+ (brain-only) or as a dependency of polymath.
6
+
7
+ Subcommands:
8
+ nexus init Create brain/ in cwd
9
+ nexus show [bucket] [--since DATE] [--until DATE] [--by OWNER] [-q PATTERN]
10
+ Search the brain. Grep-first.
11
+ nexus usage Counts per bucket + per owner
12
+ nexus own Print which slug you'll write as
13
+ nexus learn-from "<correction>" Append to rules/<you>.md (deliberate)
14
+ nexus sync Regenerate the AI-tool mirror files
15
+ nexus unmirror Remove the AI-tool mirror files
16
+ nexus spec Print the SPEC.md format contract
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ from nexus.discovery import current_owner_slug, find_brain_root, find_repo_root
26
+ from nexus.index import (
27
+ append_context, brain_grep, brain_usage, context_dir, create_project,
28
+ init_brain_root,
29
+ )
30
+ from nexus.mirrors import regenerate_mirrors, remove_mirrors
31
+
32
+
33
+ def _cmd_init(args) -> int:
34
+ base = init_brain_root(Path(args.cwd) if args.cwd else None)
35
+ repo = find_repo_root() or base.parent
36
+ report = regenerate_mirrors(repo_root=repo, force=getattr(args, "force", False))
37
+ print(f"✓ Brain initialised at {base}")
38
+ if report.written:
39
+ for tool, path in report.written.items():
40
+ print(f" ↳ mirrored to {path.relative_to(repo)} (for {tool})")
41
+ elif not report.skipped:
42
+ print(" (brain is empty; mirrors will be generated after first write)")
43
+ _print_skipped(report, repo)
44
+ print(f"\nYour owner slug: {current_owner_slug()}")
45
+ print("\nNext: `nexus learn-from \"your first rule\"`")
46
+ return 0
47
+
48
+
49
+ def _cmd_show(args) -> int:
50
+ project = args.project or ""
51
+ hits = brain_grep(
52
+ project,
53
+ pattern=args.pattern or "",
54
+ since=args.since or "",
55
+ until=args.until or "",
56
+ by=args.by or "",
57
+ ctx_name=args.bucket or "",
58
+ )
59
+ if not hits:
60
+ print("(no matches)")
61
+ return 0
62
+ if args.json:
63
+ print(json.dumps(hits, indent=2))
64
+ return 0
65
+ for h in hits:
66
+ print(f"{h['file']:40s} {h['text']}")
67
+ return 0
68
+
69
+
70
+ def _cmd_usage(args) -> int:
71
+ project = args.project or ""
72
+ stats = brain_usage(project)
73
+ if args.json:
74
+ print(json.dumps(stats, indent=2))
75
+ return 0
76
+ print(f"Total entries: {stats['total_entries']}\n")
77
+ if stats["buckets"]:
78
+ print("By bucket:")
79
+ for k, v in sorted(stats["buckets"].items(), key=lambda kv: -kv[1]):
80
+ print(f" {k:12s} {v}")
81
+ if stats["owners"]:
82
+ print("\nBy owner:")
83
+ for k, v in sorted(stats["owners"].items(), key=lambda kv: -kv[1]):
84
+ print(f" {k:12s} {v}")
85
+ return 0
86
+
87
+
88
+ def _cmd_own(args) -> int:
89
+ """Print owner slug and the timezone we'll record on writes."""
90
+ from nexus.memory.store import _local_tz_name
91
+ print(f"owner: {current_owner_slug()}")
92
+ print(f"tz: {_local_tz_name()}")
93
+ return 0
94
+
95
+
96
+ def _cmd_learn_from(args) -> int:
97
+ text = args.text or ""
98
+ if not text:
99
+ print("usage: nexus learn-from \"<rule>\"", file=sys.stderr)
100
+ return 2
101
+ project = args.project or ""
102
+ brain = find_brain_root()
103
+ if brain is None and not project:
104
+ # Need either a brain/ in cwd or a project name for personal scope.
105
+ print("error: no brain/ found in this repo. Run `nexus init` first,\n"
106
+ "or use --project <name> for personal-scope brains.", file=sys.stderr)
107
+ return 1
108
+ if not brain and project:
109
+ create_project(project)
110
+ owner = current_owner_slug()
111
+ written = append_context(project, "rules", text, by_owner=True, author=owner)
112
+ print(f"✓ Logged to {written.relative_to(written.parents[2]) if len(written.parents) >= 3 else written}")
113
+ _dt = __import__("datetime").datetime
114
+ today = _dt.now().astimezone().strftime("%Y-%m-%d")
115
+ print(f" - [{today} {owner}] {text}")
116
+ return 0
117
+
118
+
119
+ def _print_skipped(report, repo: Path) -> None:
120
+ """Tell the user about mirror files we refused to overwrite. Silence
121
+ here would look like the mirror simply worked."""
122
+ for tool, path in report.backed_up.items():
123
+ print(f" ↳ saved your old {tool} file to {path.relative_to(repo)}")
124
+ if not report.skipped:
125
+ return
126
+ for tool, path in report.skipped.items():
127
+ print(f" skipped {path.relative_to(repo)} — you wrote this, not nexus")
128
+ print(" Move its content into brain/, or run with --force to hand it "
129
+ "over (your version is saved as a .bak first).")
130
+
131
+
132
+ def _cmd_sync(args) -> int:
133
+ repo = find_repo_root() or (find_brain_root() or Path.cwd()).parent
134
+ report = regenerate_mirrors(args.project or "", repo_root=repo, force=args.force)
135
+ if not report.written and not report.skipped:
136
+ print("(brain is empty; nothing to mirror)")
137
+ return 0
138
+ for tool, path in report.written.items():
139
+ print(f"✓ {tool}: {path.relative_to(repo)}")
140
+ _print_skipped(report, repo)
141
+ return 0
142
+
143
+
144
+ def _cmd_unmirror(args) -> int:
145
+ report = remove_mirrors(force=args.force)
146
+ for p in report.removed:
147
+ print(f"removed {p}")
148
+ for p in report.skipped:
149
+ print(f"kept {p} — you wrote this, not nexus")
150
+ if report.skipped:
151
+ print("Pass --force to delete it anyway.")
152
+ if not report.removed and not report.skipped:
153
+ print("(no mirror files present)")
154
+ return 0
155
+
156
+
157
+ def _cmd_spec(args) -> int:
158
+ spec = Path(__file__).resolve().parent / "SPEC.md"
159
+ if spec.exists():
160
+ print(spec.read_text())
161
+ return 0
162
+ print(f"SPEC.md not found at {spec}", file=sys.stderr)
163
+ return 1
164
+
165
+
166
+ def build_parser() -> argparse.ArgumentParser:
167
+ p = argparse.ArgumentParser(
168
+ prog="nexus",
169
+ description="Long-term, team-shareable project brain.",
170
+ )
171
+ p.add_argument("--project", default="", help="project name (personal scope)")
172
+ sub = p.add_subparsers(dest="cmd", required=True)
173
+
174
+ p_init = sub.add_parser("init", help="create brain/ in cwd")
175
+ p_init.add_argument("--cwd", default="", help="target directory (default: cwd)")
176
+ p_init.add_argument("--force", action="store_true",
177
+ help="take over mirror files you wrote by hand (saves a .bak)")
178
+ p_init.set_defaults(func=_cmd_init)
179
+
180
+ p_show = sub.add_parser("show", help="search the brain (grep-first)")
181
+ p_show.add_argument("bucket", nargs="?", default="", help="one of: rules logic code stack data goals decisions glossary personas")
182
+ p_show.add_argument("--since", default="", help="YYYY-MM-DD lower bound")
183
+ p_show.add_argument("--until", default="", help="YYYY-MM-DD upper bound")
184
+ p_show.add_argument("--by", default="", help="owner slug")
185
+ p_show.add_argument("-q", "--pattern", default="", help="substring filter (case-insensitive)")
186
+ p_show.add_argument("--json", action="store_true", help="emit JSON")
187
+ p_show.set_defaults(func=_cmd_show)
188
+
189
+ p_usage = sub.add_parser("usage", help="counts per bucket and per owner (read-only)")
190
+ p_usage.add_argument("--json", action="store_true")
191
+ p_usage.set_defaults(func=_cmd_usage)
192
+
193
+ p_own = sub.add_parser("own", help="print which owner slug you'll write as")
194
+ p_own.set_defaults(func=_cmd_own)
195
+
196
+ p_learn = sub.add_parser("learn-from", help="log a correction to rules/<you>.md")
197
+ p_learn.add_argument("text", nargs="?", default="", help="the rule to log")
198
+ p_learn.set_defaults(func=_cmd_learn_from)
199
+
200
+ p_sync = sub.add_parser("sync", help="regenerate AI-tool mirror files")
201
+ p_sync.add_argument("--force", action="store_true",
202
+ help="take over mirror files you wrote by hand (saves a .bak)")
203
+ p_sync.set_defaults(func=_cmd_sync)
204
+
205
+ p_unmirror = sub.add_parser("unmirror", help="remove AI-tool mirror files")
206
+ p_unmirror.add_argument("--force", action="store_true",
207
+ help="also delete mirror files nexus did not write")
208
+ p_unmirror.set_defaults(func=_cmd_unmirror)
209
+
210
+ p_spec = sub.add_parser("spec", help="print the brain SPEC.md")
211
+ p_spec.set_defaults(func=_cmd_spec)
212
+
213
+ return p
214
+
215
+
216
+ def main(argv: list[str] | None = None) -> int:
217
+ parser = build_parser()
218
+ args = parser.parse_args(argv)
219
+ return args.func(args)
220
+
221
+
222
+ if __name__ == "__main__":
223
+ sys.exit(main())
nexus/config.py ADDED
@@ -0,0 +1,45 @@
1
+ """
2
+ Brain-owned configuration. Independent of polymath.config — that's the
3
+ whole point of the decoupling.
4
+
5
+ Brain data has two homes:
6
+ 1. In-repo: <repo>/brain/ ← the canonical, shared brain
7
+ 2. Personal: $POLYMATH_BRAIN_HOME or ~/.brain/projects/<name>/
8
+ ← personal scratch / no-repo
9
+ projects
10
+
11
+ Long-term contract: BRAIN_HOME lives at ~/.brain/, NOT ~/.polymath/. The
12
+ brain owns its own home directory — it must not depend on Polymath being
13
+ installed to find its own data.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from pathlib import Path
19
+
20
+
21
+ def _brain_home() -> Path:
22
+ """Personal-scope brain root. Override via $POLYMATH_BRAIN_HOME."""
23
+ override = os.environ.get("POLYMATH_BRAIN_HOME")
24
+ if override:
25
+ return Path(override).expanduser().resolve()
26
+ return Path.home() / ".brain"
27
+
28
+
29
+ BRAIN_HOME = _brain_home()
30
+ BRAIN_PROJECTS_DIR = BRAIN_HOME / "projects"
31
+ BRAIN_CACHE_DIR = BRAIN_HOME / "cache"
32
+
33
+ # In-repo brain folder name. Always exactly "brain" — this is part of the
34
+ # format SPEC and tools downstream rely on the name. Do not parameterise.
35
+ BRAIN_DIR_NAME = "brain"
36
+
37
+ # Mirror file names. These are what tools downstream auto-discover. The
38
+ # brain writes them; we never read them back (the brain is the source of
39
+ # truth, the mirrors are derived artifacts).
40
+ MIRROR_FILES = {
41
+ "claude": "CLAUDE.md",
42
+ "agents": "AGENTS.md",
43
+ "cursor": ".cursor/rules/brain.mdc",
44
+ "copilot": ".github/copilot-instructions.md",
45
+ }
nexus/discovery.py ADDED
@@ -0,0 +1,77 @@
1
+ """
2
+ Brain discovery — find a brain in the cwd hierarchy, or fall back to
3
+ personal/no-repo scope.
4
+
5
+ This is intentionally a small, dependency-free module. Anything that needs
6
+ to locate a brain (brain CLI, Polymath orchestrator, third-party tool)
7
+ calls into here. Long-term-stable: the discovery rules are part of the
8
+ SPEC.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import re
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ from nexus.config import BRAIN_DIR_NAME, BRAIN_PROJECTS_DIR
18
+
19
+
20
+ def find_brain_root(start: Path | None = None) -> Path | None:
21
+ """
22
+ Walk upward from `start` looking for a `brain/` directory. Mirrors
23
+ git's discovery semantics. Returns the absolute path or None.
24
+
25
+ Resolution: every parent up to the filesystem root. The first match
26
+ wins. This means nested repos containing a `brain/` folder are
27
+ discovered correctly from a sub-directory.
28
+ """
29
+ cwd = Path(start or Path.cwd()).resolve()
30
+ for directory in [cwd, *cwd.parents]:
31
+ candidate = directory / BRAIN_DIR_NAME
32
+ if candidate.is_dir():
33
+ return candidate
34
+ return None
35
+
36
+
37
+ def current_owner_slug() -> str:
38
+ """
39
+ Derive a stable owner slug for the current user. Used to namespace
40
+ per-owner files (rules/ayushi.md, decisions/akhil.md) so two devs
41
+ writing simultaneously never collide.
42
+
43
+ Resolution order:
44
+ 1. $POLYMATH_BRAIN_OWNER (explicit override)
45
+ 2. git config user.email → local part → sanitised slug
46
+ 3. $USER
47
+ 4. "anon" (never empty)
48
+ """
49
+ override = os.environ.get("POLYMATH_BRAIN_OWNER", "").strip()
50
+ if override:
51
+ return _slugify(override)
52
+
53
+ email = ""
54
+ try:
55
+ email = subprocess.run(
56
+ ["git", "config", "--get", "user.email"],
57
+ capture_output=True, text=True, timeout=2,
58
+ ).stdout.strip()
59
+ except Exception:
60
+ email = ""
61
+ raw = email.split("@")[0] if email else os.environ.get("USER", "")
62
+ return _slugify(raw or "anon")
63
+
64
+
65
+ def _slugify(raw: str) -> str:
66
+ slug = re.sub(r"[^a-z0-9]+", "-", (raw or "anon").lower()).strip("-")
67
+ return slug or "anon"
68
+
69
+
70
+ def find_repo_root(start: Path | None = None) -> Path | None:
71
+ """Walk upward looking for `.git/`. Used to anchor mirror files at the
72
+ repo root (CLAUDE.md, AGENTS.md, etc.)."""
73
+ cwd = Path(start or Path.cwd()).resolve()
74
+ for directory in [cwd, *cwd.parents]:
75
+ if (directory / ".git").exists():
76
+ return directory
77
+ return None