context-graph-viewer 0.1.0__tar.gz

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.
@@ -0,0 +1,189 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+
177
+ # VS Code
178
+ .vscode/
179
+
180
+
181
+ # Local agent hook configuration
182
+ /.codex/
183
+ /.claude/settings.local.json
184
+
185
+ # Project specific files
186
+ /enterprise-context/sic-agent/sic-scrapper/output/*
187
+
188
+ # deepeval writes run caches and telemetry here; build artifacts, not source.
189
+ .deepeval/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Memgraph
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.5
2
+ Name: context-graph-viewer
3
+ Version: 0.1.0
4
+ Summary: Read-only terminal viewer for a Context Graph: capture health, pipeline state, session anatomy and the knowledge graph
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Classifier: Programming Language :: Python :: 3.10
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: memgraph-context-graph>=0.6.0
13
+ Requires-Dist: memgraph-toolbox
14
+ Requires-Dist: textual<9,>=8.2
15
+ Provides-Extra: test
16
+ Requires-Dist: actions-graph[memgraph-context-graph]>=0.5.0; extra == 'test'
17
+ Requires-Dist: pytest-asyncio>=0.26.0; extra == 'test'
18
+ Requires-Dist: pytest>=9.0.3; extra == 'test'
19
+ Requires-Dist: resources-graph[memgraph-context-graph]; extra == 'test'
20
+ Requires-Dist: sessions-graph[memgraph-context-graph]>=0.8.1; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # context-graph-viewer
24
+
25
+ A read-only terminal viewer for your Context Graph. It shows what your Claude Code / Codex hooks captured, whether
26
+ capture is failing, where reconciliation stands, and what the graph learned, without Cypher or Memgraph Lab.
27
+
28
+ ```bash
29
+ memgraph-context-graph ui
30
+ ```
31
+
32
+ It reads the same config file as the hooks (`CONTEXT_GRAPH_CONFIG`, else `~/.config/context-graph/config.toml`),
33
+ polls Memgraph every few seconds, and never writes to the graph.
34
+
35
+ ## Install
36
+
37
+ `memgraph-context-graph setup` installs it unless you pass `--no-viewer`. It runs in the same tool environment as
38
+ `memgraph-context-graph`, so to add it by hand, add it to that tool's `uv tool install`:
39
+
40
+ ```bash
41
+ uv tool install "memgraph-context-graph[mcp]" \
42
+ --with "skills-graph[memgraph-context-graph]" \
43
+ --with "actions-graph[memgraph-context-graph]" \
44
+ --with "sessions-graph[memgraph-context-graph]" \
45
+ --with context-graph-viewer
46
+ ```
47
+
48
+ `context-graph-viewer` also installs its own command, which takes the same flags as `memgraph-context-graph ui`.
49
+
50
+ ## Views
51
+
52
+ | View | Reached by | Shows |
53
+ |---|---|---|
54
+ | **Dashboard** | start | Sessions in scope, the pipeline (reconcile and embedding queues, extraction backend, ontology version, failures, capture tokens), capture health (the local capture-failure log plus hints from the graph), and new entities per day. |
55
+ | **Session** | `enter` on a session | The timeline with subagents folded under the call that spawned them, each action's input and the resources it touched, and what the session produced (chunks, entities, memory files). |
56
+ | **Entities** | `k` on the dashboard, `g` on a session | Entities merged by name, by support (how many sessions and memory files ground them) or most recent, with their provenance. |
57
+ | **Neighbourhood** | `enter` on an entity | One entity's relations grouped by type, with the evidence behind each. `enter` on a neighbour moves there; `esc` walks back. |
58
+
59
+ ## Keys
60
+
61
+ | Key | Does |
62
+ |---|---|
63
+ | `a` | Widen the scope: your sessions → plus unattributed ones → everyone's |
64
+ | `p` | Narrow every view to one project (working directory) |
65
+ | `/` | Filter the table |
66
+ | `1`–`4` | Jump to a pane; `f` makes it fill the screen |
67
+ | `v` | Neighbourhood: relation tree or flat table |
68
+ | `+` / `-` | Neighbourhood: raise or lower the confidence floor |
69
+ | `h` / `s` | Entities: show value types (dates, amounts) / sort by most recent |
70
+ | `w` | Enterprise: next space this login can read |
71
+ | `r` | Refresh now |
72
+ | `?` | All keys |
73
+ | `esc` / `q` | Back / quit |
74
+
75
+ ## Scope
76
+
77
+ By default you see your own sessions: `identity.user_id` in Community, your Memgraph login in Enterprise. Sessions
78
+ captured before a user was configured belong to nobody (reconcile files them under an `anon-…` user), so if the
79
+ dashboard opens empty, press `a`.
80
+
81
+ ## Development
82
+
83
+ Tests run against a real Memgraph and drive the app headless:
84
+
85
+ ```bash
86
+ ./scripts/dev-memgraph.sh test context-graph-viewer
87
+ ```
@@ -0,0 +1,65 @@
1
+ # context-graph-viewer
2
+
3
+ A read-only terminal viewer for your Context Graph. It shows what your Claude Code / Codex hooks captured, whether
4
+ capture is failing, where reconciliation stands, and what the graph learned, without Cypher or Memgraph Lab.
5
+
6
+ ```bash
7
+ memgraph-context-graph ui
8
+ ```
9
+
10
+ It reads the same config file as the hooks (`CONTEXT_GRAPH_CONFIG`, else `~/.config/context-graph/config.toml`),
11
+ polls Memgraph every few seconds, and never writes to the graph.
12
+
13
+ ## Install
14
+
15
+ `memgraph-context-graph setup` installs it unless you pass `--no-viewer`. It runs in the same tool environment as
16
+ `memgraph-context-graph`, so to add it by hand, add it to that tool's `uv tool install`:
17
+
18
+ ```bash
19
+ uv tool install "memgraph-context-graph[mcp]" \
20
+ --with "skills-graph[memgraph-context-graph]" \
21
+ --with "actions-graph[memgraph-context-graph]" \
22
+ --with "sessions-graph[memgraph-context-graph]" \
23
+ --with context-graph-viewer
24
+ ```
25
+
26
+ `context-graph-viewer` also installs its own command, which takes the same flags as `memgraph-context-graph ui`.
27
+
28
+ ## Views
29
+
30
+ | View | Reached by | Shows |
31
+ |---|---|---|
32
+ | **Dashboard** | start | Sessions in scope, the pipeline (reconcile and embedding queues, extraction backend, ontology version, failures, capture tokens), capture health (the local capture-failure log plus hints from the graph), and new entities per day. |
33
+ | **Session** | `enter` on a session | The timeline with subagents folded under the call that spawned them, each action's input and the resources it touched, and what the session produced (chunks, entities, memory files). |
34
+ | **Entities** | `k` on the dashboard, `g` on a session | Entities merged by name, by support (how many sessions and memory files ground them) or most recent, with their provenance. |
35
+ | **Neighbourhood** | `enter` on an entity | One entity's relations grouped by type, with the evidence behind each. `enter` on a neighbour moves there; `esc` walks back. |
36
+
37
+ ## Keys
38
+
39
+ | Key | Does |
40
+ |---|---|
41
+ | `a` | Widen the scope: your sessions → plus unattributed ones → everyone's |
42
+ | `p` | Narrow every view to one project (working directory) |
43
+ | `/` | Filter the table |
44
+ | `1`–`4` | Jump to a pane; `f` makes it fill the screen |
45
+ | `v` | Neighbourhood: relation tree or flat table |
46
+ | `+` / `-` | Neighbourhood: raise or lower the confidence floor |
47
+ | `h` / `s` | Entities: show value types (dates, amounts) / sort by most recent |
48
+ | `w` | Enterprise: next space this login can read |
49
+ | `r` | Refresh now |
50
+ | `?` | All keys |
51
+ | `esc` / `q` | Back / quit |
52
+
53
+ ## Scope
54
+
55
+ By default you see your own sessions: `identity.user_id` in Community, your Memgraph login in Enterprise. Sessions
56
+ captured before a user was configured belong to nobody (reconcile files them under an `anon-…` user), so if the
57
+ dashboard opens empty, press `a`.
58
+
59
+ ## Development
60
+
61
+ Tests run against a real Memgraph and drive the app headless:
62
+
63
+ ```bash
64
+ ./scripts/dev-memgraph.sh test context-graph-viewer
65
+ ```
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "context-graph-viewer"
3
+ version = "0.1.0"
4
+ description = "Read-only terminal viewer for a Context Graph: capture health, pipeline state, session anatomy and the knowledge graph"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.10"
8
+ classifiers = [
9
+ "Programming Language :: Python :: 3.10",
10
+ "Programming Language :: Python :: 3.11",
11
+ "Programming Language :: Python :: 3.12",
12
+ "Programming Language :: Python :: 3.13",
13
+ ]
14
+ dependencies = [
15
+ # Config, identity and spaces come from the hub, so the viewer reads exactly what the hooks write to.
16
+ "memgraph-context-graph>=0.6.0",
17
+ "memgraph-toolbox",
18
+ # Pinned to one major: Textual's maintenance has slowed and majors break widget APIs (#382).
19
+ "textual>=8.2,<9",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ test = [
24
+ "pytest>=9.0.3",
25
+ "pytest-asyncio>=0.26.0",
26
+ # The tests write sessions through the real hooks pipeline rather than hand-written Cypher.
27
+ "actions-graph[memgraph-context-graph]>=0.5.0",
28
+ "sessions-graph[memgraph-context-graph]>=0.8.1",
29
+ "resources-graph[memgraph-context-graph]",
30
+ ]
31
+
32
+ [project.scripts]
33
+ context-graph-viewer = "context_graph_viewer.cli:main"
34
+
35
+ [tool.uv.sources]
36
+ memgraph-context-graph = { workspace = true }
37
+ memgraph-toolbox = { workspace = true }
38
+ actions-graph = { workspace = true }
39
+ sessions-graph = { workspace = true }
40
+ resources-graph = { workspace = true }
41
+
42
+ [tool.pytest.ini_options]
43
+ asyncio_mode = "auto"
44
+ asyncio_default_fixture_loop_scope = "function"
45
+
46
+ [build-system]
47
+ requires = ["hatchling"]
48
+ build-backend = "hatchling.build"
@@ -0,0 +1,9 @@
1
+ """Read-only terminal viewer for a Context Graph (map #374).
2
+
3
+ Run it with ``memgraph-context-graph ui``. It reads the hooks' config file, polls Memgraph, and never writes.
4
+ """
5
+
6
+ from .app import ViewerApp
7
+ from .cli import main
8
+
9
+ __all__ = ["ViewerApp", "main"]
@@ -0,0 +1,146 @@
1
+ """The viewer app: one reader per space, a scope shared by every view, and a polling tick."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import replace
6
+ from datetime import datetime, timezone
7
+ from typing import TYPE_CHECKING, ClassVar
8
+
9
+ from textual.app import App
10
+ from textual.binding import Binding
11
+
12
+ from . import fmt, queries
13
+ from .base import PolledScreen
14
+ from .dashboard import DashboardScreen
15
+ from .source import Identity, Reader, describe
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+
20
+ POLL_SECONDS = 3.0
21
+
22
+ CSS = """
23
+ #left { width: 2fr; }
24
+ #sessions, #entities, #nb { height: 1fr; }
25
+ #side { width: 1fr; }
26
+ #banner { padding: 0 1; border: round $warning; height: auto; }
27
+ #filter { height: 3; }
28
+ .pane { border: round $primary-darken-2; padding: 0 1; height: 1fr; }
29
+ .pane:focus-within, .pane:focus { border: round $accent; }
30
+ #growth-pane { height: auto; }
31
+ #growth { height: 3; }
32
+ #timeline { height: 3fr; border: round $primary-darken-2; }
33
+ #timeline:focus { border: round $accent; }
34
+ #produced-pane { height: 1fr; max-height: 12; }
35
+ #main { width: 3fr; }
36
+ #details-pane { width: 2fr; }
37
+ #entities { width: 3fr; border: round $primary-darken-2; }
38
+ #entities:focus, #sessions:focus { border: round $accent; }
39
+ #sessions { border: round $primary-darken-2; }
40
+ #prov-pane { width: 2fr; }
41
+ #nbmain { width: 3fr; }
42
+ #nb { border: round $primary-darken-2; }
43
+ #nb:focus { border: round $accent; }
44
+ #evidence-pane { width: 2fr; }
45
+ #view-status { height: 1; padding: 0 1; background: $panel; }
46
+ ProjectPicker { align: center middle; }
47
+ #picker { width: 80%; max-width: 100; height: 70%; border: round $accent; background: $surface; padding: 0 1; }
48
+ #picker .title { text-style: bold; padding: 0 0 1 0; }
49
+ """
50
+
51
+
52
+ class ViewerApp(App[None]):
53
+ """Read-only Context Graph viewer. Opens on the dashboard and polls the active view."""
54
+
55
+ TITLE = "Context Graph"
56
+ CSS = CSS
57
+ BINDINGS: ClassVar = [
58
+ Binding("a", "widen_scope", "scope"),
59
+ Binding("w", "next_space", "space", show=False),
60
+ Binding("question_mark", "help", "keys"),
61
+ Binding("q", "quit", "quit"),
62
+ ]
63
+
64
+ def __init__(
65
+ self,
66
+ identity: Identity,
67
+ *,
68
+ poll_seconds: float = POLL_SECONDS,
69
+ clock: Callable[[], datetime] | None = None,
70
+ reader_factory: Callable[..., Reader] = Reader,
71
+ ) -> None:
72
+ """Args:
73
+ identity: Who is looking, and which spaces they may read (personal first).
74
+ poll_seconds: How often the active view reloads; 0 turns polling off (tests drive it by hand).
75
+ clock: The current time, for "3m ago" text; tests pin it.
76
+ reader_factory: Builds the reader for a space.
77
+ """
78
+ super().__init__()
79
+ self.identity = identity
80
+ self.scope = queries.Scope(user_id=identity.user_id)
81
+ self.poll_seconds = poll_seconds
82
+ self._clock = clock or (lambda: datetime.now(timezone.utc))
83
+ self._reader_factory = reader_factory
84
+ self.space_index = 0
85
+ self.reader = reader_factory(identity.spaces[0])
86
+
87
+ def now(self) -> datetime:
88
+ """The current time, from the injected clock."""
89
+ return self._clock()
90
+
91
+ def on_mount(self) -> None:
92
+ """Open the dashboard and start polling."""
93
+ self.push_screen(DashboardScreen())
94
+ if self.poll_seconds > 0:
95
+ self.set_interval(self.poll_seconds, self.poll_active)
96
+
97
+ def on_unmount(self) -> None:
98
+ """Close the connection."""
99
+ self.reader.close()
100
+
101
+ def poll_active(self) -> None:
102
+ """Reload the view on top; views underneath catch up when they're shown again."""
103
+ if isinstance(self.screen, PolledScreen):
104
+ self.screen.action_poll()
105
+
106
+ def context_line(self) -> str:
107
+ """Where, as whom, and what scope: shown under every view's title."""
108
+ space = self.identity.spaces[self.space_index]
109
+ user = self.identity.user_id or "no user configured"
110
+ parts = [f"{describe(space)}", f"{space.name} space", f"user {user}", f"scope {self.scope.mode}"]
111
+ if self.scope.project:
112
+ parts.append(f"project {fmt.project_name(self.scope.project)}")
113
+ return " · ".join(parts)
114
+
115
+ def _rescoped(self) -> None:
116
+ for screen in self.screen_stack:
117
+ if isinstance(screen, PolledScreen):
118
+ screen.refresh_titles()
119
+ self.poll_active()
120
+
121
+ def action_widen_scope(self) -> None:
122
+ """Cycle the scope: your sessions, plus unattributed ones, everyone's."""
123
+ self.scope = self.scope.widened()
124
+ self._rescoped()
125
+
126
+ def set_project(self, project: str | None) -> None:
127
+ """Narrow every view to one working directory, or None for all."""
128
+ self.scope = replace(self.scope, project=project)
129
+ self._rescoped()
130
+
131
+ def action_next_space(self) -> None:
132
+ """Enterprise: switch to the next space this login may read; Community has only one."""
133
+ if len(self.identity.spaces) < 2:
134
+ self.notify("Only one space: shared spaces need the Enterprise edition.", timeout=3)
135
+ return
136
+ self.reader.close()
137
+ self.space_index = (self.space_index + 1) % len(self.identity.spaces)
138
+ self.reader = self._reader_factory(self.identity.spaces[self.space_index])
139
+ self._rescoped()
140
+
141
+ def action_help(self) -> None:
142
+ """Toggle the key help panel."""
143
+ if self.screen.query("HelpPanel"):
144
+ self.action_hide_help_panel()
145
+ else:
146
+ self.action_show_help_panel()