mnem-suite 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.
- mnem_suite-0.1.0/LICENSE +21 -0
- mnem_suite-0.1.0/PKG-INFO +153 -0
- mnem_suite-0.1.0/README.md +109 -0
- mnem_suite-0.1.0/pyproject.toml +43 -0
- mnem_suite-0.1.0/setup.cfg +4 -0
- mnem_suite-0.1.0/src/mnem/__init__.py +3 -0
- mnem_suite-0.1.0/src/mnem/__main__.py +4 -0
- mnem_suite-0.1.0/src/mnem/cli.py +114 -0
- mnem_suite-0.1.0/src/mnem/commands/__init__.py +1 -0
- mnem_suite-0.1.0/src/mnem/commands/doctor.py +122 -0
- mnem_suite-0.1.0/src/mnem/commands/hello.py +67 -0
- mnem_suite-0.1.0/src/mnem/commands/passthrough.py +107 -0
- mnem_suite-0.1.0/src/mnem/commands/version.py +87 -0
- mnem_suite-0.1.0/src/mnem/conventions.py +205 -0
- mnem_suite-0.1.0/src/mnem/failure.py +182 -0
- mnem_suite-0.1.0/src/mnem/router.py +91 -0
- mnem_suite-0.1.0/src/mnem_suite.egg-info/PKG-INFO +153 -0
- mnem_suite-0.1.0/src/mnem_suite.egg-info/SOURCES.txt +25 -0
- mnem_suite-0.1.0/src/mnem_suite.egg-info/dependency_links.txt +1 -0
- mnem_suite-0.1.0/src/mnem_suite.egg-info/entry_points.txt +2 -0
- mnem_suite-0.1.0/src/mnem_suite.egg-info/requires.txt +4 -0
- mnem_suite-0.1.0/src/mnem_suite.egg-info/top_level.txt +1 -0
- mnem_suite-0.1.0/tests/test_cli.py +118 -0
- mnem_suite-0.1.0/tests/test_conventions.py +138 -0
- mnem_suite-0.1.0/tests/test_failure.py +163 -0
- mnem_suite-0.1.0/tests/test_integration_passthrough.py +179 -0
- mnem_suite-0.1.0/tests/test_router.py +61 -0
mnem_suite-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carl Joakim Damsleth
|
|
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,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mnem-suite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Meta-CLI and suite hub for the mnem memory suite (yaams, cognitive-ledger, owa-piggy, owa-tools)
|
|
5
|
+
Author: Carl Joakim Damsleth
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Carl Joakim Damsleth
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/damsleth/mnem
|
|
29
|
+
Project-URL: Source, https://github.com/damsleth/mnem
|
|
30
|
+
Keywords: mnem,memory,cli,agent
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: MacOS
|
|
33
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Environment :: Console
|
|
36
|
+
Classifier: Development Status :: 4 - Beta
|
|
37
|
+
Requires-Python: >=3.11
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: click>=8.1
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# mnem
|
|
46
|
+
|
|
47
|
+
[](LICENSE)
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
**A local-first memory suite for AI agents.** One install gets you a
|
|
51
|
+
two-tier memory store, an M365 read/write surface, and a single CLI
|
|
52
|
+
that ties them together. Your data stays on your machine.
|
|
53
|
+
|
|
54
|
+
`mnem` is the umbrella over four independent tools that already work
|
|
55
|
+
on their own. The umbrella adds one verb surface, one install command,
|
|
56
|
+
one place to find what is in the box.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
mnem (meta-CLI + suite hub)
|
|
60
|
+
|
|
|
61
|
+
+------------------------+------------------------+
|
|
62
|
+
| | | |
|
|
63
|
+
YAAMS cognitive-ledger owa-piggy owa-tools
|
|
64
|
+
(Tier 1 raw) (Tier 2 curated) (M365 auth) (M365 read/write)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What's in the box
|
|
68
|
+
|
|
69
|
+
| Tool | Purpose | Binaries |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| [**YAAMS**](https://github.com/damsleth/yaams) | Tier 1 raw memory store - every iMessage, mail, calendar event, GitHub issue, ingested and queryable from a single SQLite file. | `yaams` |
|
|
72
|
+
| [**cognitive-ledger**](https://github.com/damsleth/cognitive-ledger) | Tier 2 curated atomic notes engine - the gems you promote out of YAAMS and keep forever as markdown with frontmatter. | `ledger`, `ledger-obsidian`, `sheep` |
|
|
73
|
+
| [**owa-piggy**](https://github.com/damsleth/owa-piggy) | Microsoft 365 auth broker - turns your existing Outlook Web session into a reusable token. No app registration. | `owa-piggy` |
|
|
74
|
+
| [**owa-tools**](https://github.com/damsleth/owa-tools) | M365 read/write CLI suite - calendar, mail, Graph, OneDrive, scheduling, people lookup, all JSON-by-default. | `owa`, `owa-cal`, `owa-mail`, `owa-graph`, `owa-doctor`, `owa-people`, `owa-sched`, `owa-drive` |
|
|
75
|
+
|
|
76
|
+
`mnem` itself adds one more binary that routes the verbs above into
|
|
77
|
+
a single user-facing surface.
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
brew install damsleth/tap/mnem
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The Homebrew formula pulls the whole suite via dependencies. On
|
|
86
|
+
PyPI the package is `mnem-suite` (both `mnem` and `mnem-cli` were
|
|
87
|
+
already taken on PyPI by unrelated projects); the installed binary
|
|
88
|
+
is still `mnem`:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pipx install mnem-suite
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Then:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
mnem init # detect sources, write config, run a dry-run
|
|
98
|
+
mnem hello # one-screen tour of the verbs
|
|
99
|
+
mnem doctor # health check across every tool
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`mnem init` is idempotent and never edits your dotfiles.
|
|
103
|
+
|
|
104
|
+
## What can it do
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
mnem query "what did we decide at the brand kickoff?"
|
|
108
|
+
mnem ingest # all configured sources, partial-success tolerant
|
|
109
|
+
mnem promote review # interactive: promote YAAMS gems to the ledger
|
|
110
|
+
mnem mail send --to ... # owa-mail wrapper
|
|
111
|
+
mnem calendar today # owa-cal wrapper
|
|
112
|
+
mnem ledger init # bootstrap a new ledger
|
|
113
|
+
mnem auth status # owa-piggy wrapper
|
|
114
|
+
mnem doctor # aggregate health check
|
|
115
|
+
mnem version # own version + observed component versions
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Every JSON-capable command accepts `--json` (machine mode) and
|
|
119
|
+
`--pretty` (human rendering). Exit codes are predictable per
|
|
120
|
+
[CONVENTIONS.md](CONVENTIONS.md): 0 ok, 1 user error, 2 transient,
|
|
121
|
+
3 auth, 4 not found, 5 partial success.
|
|
122
|
+
|
|
123
|
+
## First day
|
|
124
|
+
|
|
125
|
+
1. `brew install damsleth/tap/mnem`
|
|
126
|
+
2. `mnem init` - the wizard probes for iMessage, Apple Mail, Signal,
|
|
127
|
+
GitHub, owa-piggy, Obsidian, and an existing cognitive-ledger. It
|
|
128
|
+
enables what it finds and writes `enabled: false` with a hint for
|
|
129
|
+
what it doesn't.
|
|
130
|
+
3. `mnem ingest` - first run downloads embedding models (~2 GB) with
|
|
131
|
+
a prompt before any download.
|
|
132
|
+
4. `mnem query "..."` - ask the suite anything.
|
|
133
|
+
|
|
134
|
+
See [SUITE.md](SUITE.md) for the full data flow and architecture, and
|
|
135
|
+
[CONVENTIONS.md](CONVENTIONS.md) for the CLI contract every tool in
|
|
136
|
+
the suite conforms to.
|
|
137
|
+
|
|
138
|
+
## Skills
|
|
139
|
+
|
|
140
|
+
Two agent skill repos sit on top of `mnem`:
|
|
141
|
+
|
|
142
|
+
- [`damsleth/SKILLS`](https://github.com/damsleth/SKILLS) - public,
|
|
143
|
+
reusable agent skills. Includes `/memory`, which routes through
|
|
144
|
+
`mnem`.
|
|
145
|
+
- `damsleth/SKILLS-private` - personal-infra `cj-*` skills (timereg,
|
|
146
|
+
did, weekly review). Same installer pattern; private repo.
|
|
147
|
+
|
|
148
|
+
Skills wrap `mnem`. `mnem` does not call skills. One direction, no
|
|
149
|
+
circular dependencies.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# mnem
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
**A local-first memory suite for AI agents.** One install gets you a
|
|
7
|
+
two-tier memory store, an M365 read/write surface, and a single CLI
|
|
8
|
+
that ties them together. Your data stays on your machine.
|
|
9
|
+
|
|
10
|
+
`mnem` is the umbrella over four independent tools that already work
|
|
11
|
+
on their own. The umbrella adds one verb surface, one install command,
|
|
12
|
+
one place to find what is in the box.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
mnem (meta-CLI + suite hub)
|
|
16
|
+
|
|
|
17
|
+
+------------------------+------------------------+
|
|
18
|
+
| | | |
|
|
19
|
+
YAAMS cognitive-ledger owa-piggy owa-tools
|
|
20
|
+
(Tier 1 raw) (Tier 2 curated) (M365 auth) (M365 read/write)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What's in the box
|
|
24
|
+
|
|
25
|
+
| Tool | Purpose | Binaries |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| [**YAAMS**](https://github.com/damsleth/yaams) | Tier 1 raw memory store - every iMessage, mail, calendar event, GitHub issue, ingested and queryable from a single SQLite file. | `yaams` |
|
|
28
|
+
| [**cognitive-ledger**](https://github.com/damsleth/cognitive-ledger) | Tier 2 curated atomic notes engine - the gems you promote out of YAAMS and keep forever as markdown with frontmatter. | `ledger`, `ledger-obsidian`, `sheep` |
|
|
29
|
+
| [**owa-piggy**](https://github.com/damsleth/owa-piggy) | Microsoft 365 auth broker - turns your existing Outlook Web session into a reusable token. No app registration. | `owa-piggy` |
|
|
30
|
+
| [**owa-tools**](https://github.com/damsleth/owa-tools) | M365 read/write CLI suite - calendar, mail, Graph, OneDrive, scheduling, people lookup, all JSON-by-default. | `owa`, `owa-cal`, `owa-mail`, `owa-graph`, `owa-doctor`, `owa-people`, `owa-sched`, `owa-drive` |
|
|
31
|
+
|
|
32
|
+
`mnem` itself adds one more binary that routes the verbs above into
|
|
33
|
+
a single user-facing surface.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
brew install damsleth/tap/mnem
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The Homebrew formula pulls the whole suite via dependencies. On
|
|
42
|
+
PyPI the package is `mnem-suite` (both `mnem` and `mnem-cli` were
|
|
43
|
+
already taken on PyPI by unrelated projects); the installed binary
|
|
44
|
+
is still `mnem`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pipx install mnem-suite
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
mnem init # detect sources, write config, run a dry-run
|
|
54
|
+
mnem hello # one-screen tour of the verbs
|
|
55
|
+
mnem doctor # health check across every tool
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`mnem init` is idempotent and never edits your dotfiles.
|
|
59
|
+
|
|
60
|
+
## What can it do
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
mnem query "what did we decide at the brand kickoff?"
|
|
64
|
+
mnem ingest # all configured sources, partial-success tolerant
|
|
65
|
+
mnem promote review # interactive: promote YAAMS gems to the ledger
|
|
66
|
+
mnem mail send --to ... # owa-mail wrapper
|
|
67
|
+
mnem calendar today # owa-cal wrapper
|
|
68
|
+
mnem ledger init # bootstrap a new ledger
|
|
69
|
+
mnem auth status # owa-piggy wrapper
|
|
70
|
+
mnem doctor # aggregate health check
|
|
71
|
+
mnem version # own version + observed component versions
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Every JSON-capable command accepts `--json` (machine mode) and
|
|
75
|
+
`--pretty` (human rendering). Exit codes are predictable per
|
|
76
|
+
[CONVENTIONS.md](CONVENTIONS.md): 0 ok, 1 user error, 2 transient,
|
|
77
|
+
3 auth, 4 not found, 5 partial success.
|
|
78
|
+
|
|
79
|
+
## First day
|
|
80
|
+
|
|
81
|
+
1. `brew install damsleth/tap/mnem`
|
|
82
|
+
2. `mnem init` - the wizard probes for iMessage, Apple Mail, Signal,
|
|
83
|
+
GitHub, owa-piggy, Obsidian, and an existing cognitive-ledger. It
|
|
84
|
+
enables what it finds and writes `enabled: false` with a hint for
|
|
85
|
+
what it doesn't.
|
|
86
|
+
3. `mnem ingest` - first run downloads embedding models (~2 GB) with
|
|
87
|
+
a prompt before any download.
|
|
88
|
+
4. `mnem query "..."` - ask the suite anything.
|
|
89
|
+
|
|
90
|
+
See [SUITE.md](SUITE.md) for the full data flow and architecture, and
|
|
91
|
+
[CONVENTIONS.md](CONVENTIONS.md) for the CLI contract every tool in
|
|
92
|
+
the suite conforms to.
|
|
93
|
+
|
|
94
|
+
## Skills
|
|
95
|
+
|
|
96
|
+
Two agent skill repos sit on top of `mnem`:
|
|
97
|
+
|
|
98
|
+
- [`damsleth/SKILLS`](https://github.com/damsleth/SKILLS) - public,
|
|
99
|
+
reusable agent skills. Includes `/memory`, which routes through
|
|
100
|
+
`mnem`.
|
|
101
|
+
- `damsleth/SKILLS-private` - personal-infra `cj-*` skills (timereg,
|
|
102
|
+
did, weekly review). Same installer pattern; private repo.
|
|
103
|
+
|
|
104
|
+
Skills wrap `mnem`. `mnem` does not call skills. One direction, no
|
|
105
|
+
circular dependencies.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mnem-suite"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Meta-CLI and suite hub for the mnem memory suite (yaams, cognitive-ledger, owa-piggy, owa-tools)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Carl Joakim Damsleth" }]
|
|
13
|
+
keywords = ["mnem", "memory", "cli", "agent"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: MacOS",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"click>=8.1",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/damsleth/mnem"
|
|
28
|
+
Source = "https://github.com/damsleth/mnem"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
mnem = "mnem.cli:main"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
include = ["mnem*"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""mnem Click root.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
- hello - one-screen elevator pitch
|
|
5
|
+
- version - mnem version + observed component versions
|
|
6
|
+
- doctor - aggregate health check across the suite
|
|
7
|
+
- query - passthrough to `yaams query` (with --tier aliasing)
|
|
8
|
+
- ingest - passthrough to `yaams ingest`
|
|
9
|
+
- promote, ledger, mail, calendar, auth: Phase 3b
|
|
10
|
+
|
|
11
|
+
Top-level flags: --version (Click default), --doctor, --json,
|
|
12
|
+
--verbose. The doctor flag is wired so `mnem --doctor` works for
|
|
13
|
+
parity with other binaries in the suite.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from mnem import __version__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group(
|
|
26
|
+
invoke_without_command=True,
|
|
27
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
28
|
+
)
|
|
29
|
+
@click.version_option(__version__, prog_name="mnem")
|
|
30
|
+
@click.option(
|
|
31
|
+
"--doctor",
|
|
32
|
+
is_flag=True,
|
|
33
|
+
default=False,
|
|
34
|
+
help="Run health check across the suite and exit.",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--json",
|
|
38
|
+
"as_json_top",
|
|
39
|
+
is_flag=True,
|
|
40
|
+
default=False,
|
|
41
|
+
help="Machine mode (JSON output) for top-level commands.",
|
|
42
|
+
)
|
|
43
|
+
@click.option(
|
|
44
|
+
"-v",
|
|
45
|
+
"--verbose",
|
|
46
|
+
is_flag=True,
|
|
47
|
+
default=False,
|
|
48
|
+
help="Verbose mode: dump captured stderr from subprocess failures.",
|
|
49
|
+
)
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def cli(ctx: click.Context, doctor: bool, as_json_top: bool, verbose: bool) -> None:
|
|
52
|
+
ctx.ensure_object(dict)
|
|
53
|
+
ctx.obj["json"] = as_json_top
|
|
54
|
+
ctx.obj["verbose"] = verbose
|
|
55
|
+
if doctor:
|
|
56
|
+
from mnem.commands.doctor import run as doctor_run
|
|
57
|
+
ctx.exit(doctor_run(as_json_top))
|
|
58
|
+
if ctx.invoked_subcommand is None:
|
|
59
|
+
from mnem.commands.hello import run as hello_run
|
|
60
|
+
ctx.exit(hello_run(as_json_top))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@cli.command("hello")
|
|
64
|
+
@click.option("--json", "as_json", is_flag=True, default=False)
|
|
65
|
+
@click.pass_context
|
|
66
|
+
def hello_cmd(ctx: click.Context, as_json: bool) -> None:
|
|
67
|
+
from mnem.commands.hello import run
|
|
68
|
+
ctx.exit(run(as_json or ctx.obj.get("json", False)))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@cli.command("version")
|
|
72
|
+
@click.option("--json", "as_json", is_flag=True, default=False)
|
|
73
|
+
@click.pass_context
|
|
74
|
+
def version_cmd(ctx: click.Context, as_json: bool) -> None:
|
|
75
|
+
from mnem.commands.version import run
|
|
76
|
+
ctx.exit(run(as_json or ctx.obj.get("json", False)))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@cli.command("doctor")
|
|
80
|
+
@click.option("--json", "as_json", is_flag=True, default=False)
|
|
81
|
+
@click.pass_context
|
|
82
|
+
def doctor_cmd(ctx: click.Context, as_json: bool) -> None:
|
|
83
|
+
from mnem.commands.doctor import run
|
|
84
|
+
ctx.exit(run(as_json or ctx.obj.get("json", False)))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@cli.command(
|
|
88
|
+
"query",
|
|
89
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
90
|
+
)
|
|
91
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
92
|
+
@click.pass_context
|
|
93
|
+
def query_cmd(ctx: click.Context, args: tuple[str, ...]) -> None:
|
|
94
|
+
from mnem.commands.passthrough import run
|
|
95
|
+
ctx.exit(run(["query", *args], verbose=ctx.obj.get("verbose", False)))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli.command(
|
|
99
|
+
"ingest",
|
|
100
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
101
|
+
)
|
|
102
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
103
|
+
@click.pass_context
|
|
104
|
+
def ingest_cmd(ctx: click.Context, args: tuple[str, ...]) -> None:
|
|
105
|
+
from mnem.commands.passthrough import run
|
|
106
|
+
ctx.exit(run(["ingest", *args], verbose=ctx.obj.get("verbose", False)))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main() -> int:
|
|
110
|
+
return cli(standalone_mode=False) or 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""mnem subcommand implementations."""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""``mnem doctor`` - aggregate health check across the suite.
|
|
2
|
+
|
|
3
|
+
Output class: data. Fans out to each `<tool> --doctor --json` and
|
|
4
|
+
collects findings into one document.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from typing import TextIO
|
|
12
|
+
|
|
13
|
+
from mnem import __version__
|
|
14
|
+
from mnem.failure import run_subprocess
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Order is the report order; mnem first, then tiers, then M365.
|
|
18
|
+
_FANOUT = [
|
|
19
|
+
"yaams",
|
|
20
|
+
"ledger",
|
|
21
|
+
"sheep",
|
|
22
|
+
"ledger-obsidian",
|
|
23
|
+
"owa-piggy",
|
|
24
|
+
"owa-cal",
|
|
25
|
+
"owa-mail",
|
|
26
|
+
"owa-graph",
|
|
27
|
+
"owa-people",
|
|
28
|
+
"owa-sched",
|
|
29
|
+
"owa-drive",
|
|
30
|
+
"owa",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _probe(binary: str) -> dict:
|
|
35
|
+
"""Probe `<binary> --doctor --json` and return the parsed payload.
|
|
36
|
+
|
|
37
|
+
On crash or non-JSON output, synthesises a stub payload that
|
|
38
|
+
preserves the doctor-schema invariants (tool name, findings list).
|
|
39
|
+
"""
|
|
40
|
+
result = run_subprocess([binary, "--doctor"], tool=binary, inject_json=True)
|
|
41
|
+
if result.crashed:
|
|
42
|
+
return {
|
|
43
|
+
"tool": binary,
|
|
44
|
+
"version": None,
|
|
45
|
+
"installed": False,
|
|
46
|
+
"findings": [
|
|
47
|
+
{
|
|
48
|
+
"id": "binary_missing",
|
|
49
|
+
"severity": "error",
|
|
50
|
+
"message": "binary not on PATH or crashed before emitting JSON",
|
|
51
|
+
"hint": f"brew install damsleth/tap/{binary} (or check PATH)",
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
env = result.stdout_envelope or {}
|
|
56
|
+
env["installed"] = True
|
|
57
|
+
# Each binary's doctor exit code influenced findings already.
|
|
58
|
+
# Track the raw exit_code for the aggregator's severity rollup.
|
|
59
|
+
env["exit_code"] = result.returncode
|
|
60
|
+
return env
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _aggregate() -> dict:
|
|
64
|
+
components = []
|
|
65
|
+
worst_exit = 0
|
|
66
|
+
for binary in _FANOUT:
|
|
67
|
+
payload = _probe(binary)
|
|
68
|
+
components.append(payload)
|
|
69
|
+
# Aggregate severity from two sources:
|
|
70
|
+
# (1) the subprocess returncode (clamped to the standard set), and
|
|
71
|
+
# (2) the findings list - an error-severity finding always bumps
|
|
72
|
+
# exit to at least 1, even if the binary itself returned 0.
|
|
73
|
+
sub_exit = int(payload.get("exit_code") or 0)
|
|
74
|
+
if not payload.get("installed", False):
|
|
75
|
+
# Missing binary -> user-fixable (install or PATH); not the raw
|
|
76
|
+
# FileNotFoundError exit (127).
|
|
77
|
+
sub_exit = 1
|
|
78
|
+
severities = {f.get("severity") for f in (payload.get("findings") or [])}
|
|
79
|
+
if "error" in severities:
|
|
80
|
+
sub_exit = max(sub_exit, 1)
|
|
81
|
+
worst_exit = max(worst_exit, sub_exit)
|
|
82
|
+
return {
|
|
83
|
+
"tool": "mnem",
|
|
84
|
+
"version": __version__,
|
|
85
|
+
"components": components,
|
|
86
|
+
"_exit_code": worst_exit,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run(as_json: bool, stream: TextIO | None = None) -> int:
|
|
91
|
+
if stream is None:
|
|
92
|
+
stream = sys.stdout
|
|
93
|
+
doc = _aggregate()
|
|
94
|
+
exit_code = int(doc.pop("_exit_code", 0))
|
|
95
|
+
if as_json:
|
|
96
|
+
stream.write(json.dumps(doc, ensure_ascii=False) + "\n")
|
|
97
|
+
stream.flush()
|
|
98
|
+
return exit_code
|
|
99
|
+
|
|
100
|
+
stream.write(f"mnem doctor (v{doc['version']})\n")
|
|
101
|
+
for comp in doc["components"]:
|
|
102
|
+
name = comp["tool"]
|
|
103
|
+
if not comp.get("installed"):
|
|
104
|
+
stream.write(f" {name:<18} - not installed\n")
|
|
105
|
+
continue
|
|
106
|
+
findings = comp.get("findings") or []
|
|
107
|
+
if not findings:
|
|
108
|
+
stream.write(f" {name:<18} ok\n")
|
|
109
|
+
continue
|
|
110
|
+
severities = {f["severity"] for f in findings}
|
|
111
|
+
if "error" in severities:
|
|
112
|
+
mark = "x"
|
|
113
|
+
elif "warning" in severities:
|
|
114
|
+
mark = "!"
|
|
115
|
+
else:
|
|
116
|
+
mark = "."
|
|
117
|
+
stream.write(f" {name:<18} {mark} {len(findings)} finding(s)\n")
|
|
118
|
+
for f in findings:
|
|
119
|
+
hint = f" hint: {f['hint']}" if f.get("hint") else ""
|
|
120
|
+
stream.write(f" - [{f['severity']}] {f['id']}: {f['message']}{hint}\n")
|
|
121
|
+
stream.flush()
|
|
122
|
+
return exit_code
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""``mnem hello`` - the one-screen elevator pitch.
|
|
2
|
+
|
|
3
|
+
Output class: data. Runs without any config; safe on a fresh
|
|
4
|
+
install before anything is wired up. Emits JSON on stdout under
|
|
5
|
+
--json, a human banner otherwise.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import TextIO
|
|
13
|
+
|
|
14
|
+
from mnem import __version__
|
|
15
|
+
from mnem.router import verbs
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Static verbs that ship in 3a beyond the translation table.
|
|
19
|
+
_BUILTIN_VERBS = [
|
|
20
|
+
("hello", "mnem", "Show this elevator pitch"),
|
|
21
|
+
("version", "mnem", "Show mnem version and observed component versions"),
|
|
22
|
+
("doctor", "mnem", "Run health checks across the whole suite"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _all_verbs() -> list[tuple[str, str, str]]:
|
|
27
|
+
return _BUILTIN_VERBS + verbs()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _data_doc() -> dict:
|
|
31
|
+
return {
|
|
32
|
+
"tool": "mnem",
|
|
33
|
+
"version": __version__,
|
|
34
|
+
"tagline": "Local-first memory suite for AI agents.",
|
|
35
|
+
"verbs": [
|
|
36
|
+
{"verb": verb, "binary": binary, "description": desc}
|
|
37
|
+
for (verb, binary, desc) in _all_verbs()
|
|
38
|
+
],
|
|
39
|
+
"next_steps": [
|
|
40
|
+
"mnem doctor",
|
|
41
|
+
"mnem query \"<question>\"",
|
|
42
|
+
"mnem ingest",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run(as_json: bool, stream: TextIO | None = None) -> int:
|
|
48
|
+
if stream is None:
|
|
49
|
+
stream = sys.stdout
|
|
50
|
+
if as_json:
|
|
51
|
+
stream.write(json.dumps(_data_doc(), ensure_ascii=False) + "\n")
|
|
52
|
+
stream.flush()
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
doc = _data_doc()
|
|
56
|
+
stream.write(f"mnem v{doc['version']} - {doc['tagline']}\n\n")
|
|
57
|
+
stream.write("Verbs:\n")
|
|
58
|
+
width = max(len(v) for (v, _, _) in doc["verbs"] if isinstance(v, str)) if doc["verbs"] else 0
|
|
59
|
+
for entry in doc["verbs"]:
|
|
60
|
+
verb = entry["verb"]
|
|
61
|
+
desc = entry["description"]
|
|
62
|
+
stream.write(f" {verb:<{width}} {desc}\n")
|
|
63
|
+
stream.write("\nNext steps:\n")
|
|
64
|
+
for cmd in doc["next_steps"]:
|
|
65
|
+
stream.write(f" $ {cmd}\n")
|
|
66
|
+
stream.flush()
|
|
67
|
+
return 0
|