amie 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.
- amie-0.1.0/LICENSE +21 -0
- amie-0.1.0/PKG-INFO +96 -0
- amie-0.1.0/README.md +45 -0
- amie-0.1.0/pyproject.toml +87 -0
- amie-0.1.0/src/amie/__init__.py +3 -0
- amie-0.1.0/src/amie/cli.py +410 -0
- amie-0.1.0/src/amie/config.py +154 -0
- amie-0.1.0/src/amie/dev.py +153 -0
- amie-0.1.0/src/amie/docs.py +75 -0
- amie-0.1.0/src/amie/doctor.py +114 -0
- amie-0.1.0/src/amie/init.py +203 -0
- amie-0.1.0/src/amie/normalizers/__init__.py +1 -0
- amie-0.1.0/src/amie/normalizers/header.py +216 -0
- amie-0.1.0/src/amie/normalizers/markor.py +93 -0
- amie-0.1.0/src/amie/security.py +15 -0
- amie-0.1.0/src/amie/vault.py +160 -0
- amie-0.1.0/src/amie/watcher.py +157 -0
amie-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nk0s1
|
|
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.
|
amie-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: amie
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Your private memory intelligence — captures thoughts from anywhere, organises them into your vault, and reflects insights back to you. Entirely local.
|
|
5
|
+
Keywords: obsidian,notes,ocr,ollama,local-ai,knowledge-management
|
|
6
|
+
Author: nk0s1
|
|
7
|
+
Author-email: nk0s1 <nkosinathi@whakatau.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 nk0s1
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
Classifier: Development Status :: 3 - Alpha
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
+
Classifier: Topic :: Office/Business :: News/Diary
|
|
38
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
39
|
+
Classifier: Topic :: Utilities
|
|
40
|
+
Requires-Dist: ollama>=0.6.2
|
|
41
|
+
Requires-Dist: pillow-heif>=1.3.0
|
|
42
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
43
|
+
Requires-Dist: python-frontmatter>=1.1.0
|
|
44
|
+
Requires-Dist: typer>=0.25.1
|
|
45
|
+
Requires-Dist: watchdog>=6.0.0
|
|
46
|
+
Requires-Python: >=3.12
|
|
47
|
+
Project-URL: Documentation, https://gitlab.com/nkosinathi1/amie/-/tree/main/docs
|
|
48
|
+
Project-URL: Issues, https://gitlab.com/nkosinathi1/amie/-/issues
|
|
49
|
+
Project-URL: Source, https://gitlab.com/nkosinathi1/amie
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# AMI — Anonymous Memory Intelligence
|
|
53
|
+
|
|
54
|
+
Your private memory intelligence — captures thoughts from anywhere, organises them into your vault, and reflects insights back to you. Entirely local.
|
|
55
|
+
|
|
56
|
+
**Status:** Alpha. The core CLI and domain modules are in place; ingestion, OCR, and the weekly-digest pipeline are still being built.
|
|
57
|
+
|
|
58
|
+
**Hard constraints:**
|
|
59
|
+
- Open-source, no paid services.
|
|
60
|
+
- Local-first AI via Ollama only — your notes never leave your device.
|
|
61
|
+
- Built for the user's existing Obsidian vault conventions (`KEY: value` headers, lifecycle states `#infant → #child → #developing → #adult → #on-ice → #removed`).
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Prereqs: Python 3.12, uv, and Ollama. See docs/01-prerequisites.md.
|
|
67
|
+
git clone git@gitlab.com:nkosinathi1/ami.git ~/source/repos/ami
|
|
68
|
+
cd ~/source/repos/ami
|
|
69
|
+
uv sync
|
|
70
|
+
ami init # one-time setup wizard
|
|
71
|
+
ami doctor # verify uv / Ollama / models / vault / ssh
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Try it
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
ami --help # discover every subcommand
|
|
78
|
+
ami doctor # health-check the environment
|
|
79
|
+
ami vault status # confirm inbox folders are in place
|
|
80
|
+
ami dev guard # lint + format check + tests + security audit
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
The build guide and operator manual live at `~/source/ubuntu/docs/ami/`:
|
|
86
|
+
|
|
87
|
+
- `00-overview.md` — start here.
|
|
88
|
+
- `standards.md` — engineering rules every contribution is held to.
|
|
89
|
+
- `security.md` — threat model.
|
|
90
|
+
- `14-cli-reference.md` — every `ami` command, what it does.
|
|
91
|
+
- `15-ide-setup.md` — VS Code + WSL setup.
|
|
92
|
+
- `activity-log.md` — append-only audit trail of every change.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT. See `LICENSE`.
|
amie-0.1.0/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# AMI — Anonymous Memory Intelligence
|
|
2
|
+
|
|
3
|
+
Your private memory intelligence — captures thoughts from anywhere, organises them into your vault, and reflects insights back to you. Entirely local.
|
|
4
|
+
|
|
5
|
+
**Status:** Alpha. The core CLI and domain modules are in place; ingestion, OCR, and the weekly-digest pipeline are still being built.
|
|
6
|
+
|
|
7
|
+
**Hard constraints:**
|
|
8
|
+
- Open-source, no paid services.
|
|
9
|
+
- Local-first AI via Ollama only — your notes never leave your device.
|
|
10
|
+
- Built for the user's existing Obsidian vault conventions (`KEY: value` headers, lifecycle states `#infant → #child → #developing → #adult → #on-ice → #removed`).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Prereqs: Python 3.12, uv, and Ollama. See docs/01-prerequisites.md.
|
|
16
|
+
git clone git@gitlab.com:nkosinathi1/ami.git ~/source/repos/ami
|
|
17
|
+
cd ~/source/repos/ami
|
|
18
|
+
uv sync
|
|
19
|
+
ami init # one-time setup wizard
|
|
20
|
+
ami doctor # verify uv / Ollama / models / vault / ssh
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Try it
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ami --help # discover every subcommand
|
|
27
|
+
ami doctor # health-check the environment
|
|
28
|
+
ami vault status # confirm inbox folders are in place
|
|
29
|
+
ami dev guard # lint + format check + tests + security audit
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
The build guide and operator manual live at `~/source/ubuntu/docs/ami/`:
|
|
35
|
+
|
|
36
|
+
- `00-overview.md` — start here.
|
|
37
|
+
- `standards.md` — engineering rules every contribution is held to.
|
|
38
|
+
- `security.md` — threat model.
|
|
39
|
+
- `14-cli-reference.md` — every `ami` command, what it does.
|
|
40
|
+
- `15-ide-setup.md` — VS Code + WSL setup.
|
|
41
|
+
- `activity-log.md` — append-only audit trail of every change.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "amie"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Your private memory intelligence — captures thoughts from anywhere, organises them into your vault, and reflects insights back to you. Entirely local."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "nk0s1", email = "nkosinathi@whakatau.com" },
|
|
9
|
+
]
|
|
10
|
+
keywords = ["obsidian", "notes", "ocr", "ollama", "local-ai", "knowledge-management"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: End Users/Desktop",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Office/Business :: News/Diary",
|
|
21
|
+
"Topic :: Text Processing :: Markup",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.12"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"ollama>=0.6.2",
|
|
27
|
+
"pillow-heif>=1.3.0",
|
|
28
|
+
"python-dotenv>=1.2.2",
|
|
29
|
+
"python-frontmatter>=1.1.0",
|
|
30
|
+
"typer>=0.25.1",
|
|
31
|
+
"watchdog>=6.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Documentation = "https://gitlab.com/nkosinathi1/amie/-/tree/main/docs"
|
|
36
|
+
Issues = "https://gitlab.com/nkosinathi1/amie/-/issues"
|
|
37
|
+
Source = "https://gitlab.com/nkosinathi1/amie"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
amie = "amie.cli:app"
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 100
|
|
44
|
+
target-version = "py312"
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["E", "F", "I", "B", "UP", "RUF"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
50
|
+
# Typer's normal pattern is `typer.Option(...)` / `typer.Argument(...)` in
|
|
51
|
+
# function defaults — these calls return immutable descriptors, not mutable
|
|
52
|
+
# objects, so B008 is a false positive here.
|
|
53
|
+
extend-immutable-calls = ["typer.Option", "typer.Argument"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
pythonpath = ["src"]
|
|
58
|
+
addopts = [
|
|
59
|
+
"--cov=amie",
|
|
60
|
+
"--cov-branch",
|
|
61
|
+
"--cov-report=term-missing",
|
|
62
|
+
"--cov-fail-under=90",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.coverage.run]
|
|
66
|
+
source = ["src/amie"]
|
|
67
|
+
branch = true
|
|
68
|
+
|
|
69
|
+
[tool.coverage.report]
|
|
70
|
+
exclude_lines = [
|
|
71
|
+
"pragma: no cover",
|
|
72
|
+
"raise NotImplementedError",
|
|
73
|
+
"if __name__ == .__main__.:",
|
|
74
|
+
"if TYPE_CHECKING:",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[build-system]
|
|
78
|
+
requires = ["uv_build>=0.11.8,<0.12.0"]
|
|
79
|
+
build-backend = "uv_build"
|
|
80
|
+
|
|
81
|
+
[dependency-groups]
|
|
82
|
+
dev = [
|
|
83
|
+
"pip-audit>=2.7",
|
|
84
|
+
"pytest>=9.0.3",
|
|
85
|
+
"pytest-cov>=7.1.0",
|
|
86
|
+
"ruff>=0.15.12",
|
|
87
|
+
]
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""Typer CLI entry point for AMI.
|
|
2
|
+
|
|
3
|
+
This module is deliberately thin — it only wires Typer to domain modules.
|
|
4
|
+
All logic lives in `amie.dev`, `amie.vault`, `ami.doctor`, `amie.init`,
|
|
5
|
+
`amie.security`. The CLI's job is two things:
|
|
6
|
+
|
|
7
|
+
1. Wire Typer subcommands to the domain.
|
|
8
|
+
2. Translate domain exceptions into friendly messages with exact next steps.
|
|
9
|
+
|
|
10
|
+
CLI tests exercise real subprocess and real filesystem; no internal mocking.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from functools import wraps
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
|
|
23
|
+
from amie import dev, security, vault
|
|
24
|
+
from amie.config import Config, ConfigError, user_config_path
|
|
25
|
+
from amie.doctor import CheckResult, run_all_checks
|
|
26
|
+
from amie.init import InitOutcome, SshSetupResult, run_init, setup_ssh_server
|
|
27
|
+
|
|
28
|
+
# Load `.env` from the working directory at CLI startup. Domain modules read
|
|
29
|
+
# only `os.environ`; this is the one place we touch the file system for env.
|
|
30
|
+
load_dotenv(override=False)
|
|
31
|
+
|
|
32
|
+
app = typer.Typer(help="AMI — Anonymous Memory Intelligence")
|
|
33
|
+
dev_app = typer.Typer(help="Developer tooling: tests, lint, format, sync.")
|
|
34
|
+
vault_app = typer.Typer(help="Manage AMI's folders inside the Obsidian vault.")
|
|
35
|
+
app.add_typer(dev_app, name="dev")
|
|
36
|
+
app.add_typer(vault_app, name="vault")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Friendly error wrapper
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def friendly(func: Callable[..., None]) -> Callable[..., None]:
|
|
45
|
+
"""Decorator: catch domain exceptions and print plain-English messages.
|
|
46
|
+
|
|
47
|
+
Domain code is allowed to be strict (raise). The CLI translates every
|
|
48
|
+
expected exception into a clear, actionable message for the user. Stack
|
|
49
|
+
traces only appear if AMIE_DEBUG=1 is set.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@wraps(func)
|
|
53
|
+
def wrapper(*args, **kwargs):
|
|
54
|
+
try:
|
|
55
|
+
return func(*args, **kwargs)
|
|
56
|
+
except ConfigError as err:
|
|
57
|
+
typer.echo("\n Hold on — AMI hasn't been set up yet on this machine.\n", err=True)
|
|
58
|
+
typer.echo(f" Reason: {err}\n", err=True)
|
|
59
|
+
typer.echo(" Run this once to fix it:", err=True)
|
|
60
|
+
typer.echo("\n amie init\n", err=True)
|
|
61
|
+
raise typer.Exit(code=1) from err
|
|
62
|
+
except FileNotFoundError as err:
|
|
63
|
+
typer.echo("\n Couldn't find a file or folder AMI needs:\n", err=True)
|
|
64
|
+
typer.echo(f" {err}\n", err=True)
|
|
65
|
+
typer.echo(" Try `amie doctor` to see what's missing.\n", err=True)
|
|
66
|
+
raise typer.Exit(code=1) from err
|
|
67
|
+
|
|
68
|
+
return wrapper
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Top-level commands
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command()
|
|
77
|
+
def install(
|
|
78
|
+
vault_path: Path | None = typer.Option(
|
|
79
|
+
None,
|
|
80
|
+
"--vault-path",
|
|
81
|
+
help="Path to your Obsidian vault. If omitted you'll be asked.",
|
|
82
|
+
),
|
|
83
|
+
yes: bool = typer.Option(
|
|
84
|
+
False, "--yes", "-y", help="Accept all defaults; create the vault if missing."
|
|
85
|
+
),
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Bootstrap AMI on this machine: install the binary and run first-time setup."""
|
|
88
|
+
import subprocess
|
|
89
|
+
|
|
90
|
+
project_root = Path(__file__).resolve().parent.parent.parent
|
|
91
|
+
rc = subprocess.run(
|
|
92
|
+
["uv", "tool", "install", "--editable", str(project_root)],
|
|
93
|
+
check=False,
|
|
94
|
+
).returncode
|
|
95
|
+
if rc != 0:
|
|
96
|
+
typer.echo(
|
|
97
|
+
"\n Binary install failed. Make sure uv is on PATH, then run:\n"
|
|
98
|
+
f"\n uv tool install --editable {project_root}\n",
|
|
99
|
+
err=True,
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(code=rc)
|
|
102
|
+
|
|
103
|
+
typer.echo("\n Binary installed. Running first-time setup...\n")
|
|
104
|
+
init(vault_path=vault_path, yes=yes)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command()
|
|
108
|
+
def init(
|
|
109
|
+
vault_path: Path | None = typer.Option(
|
|
110
|
+
None,
|
|
111
|
+
"--vault-path",
|
|
112
|
+
help="Path to your Obsidian vault. If omitted you'll be asked.",
|
|
113
|
+
),
|
|
114
|
+
yes: bool = typer.Option(
|
|
115
|
+
False, "--yes", "-y", help="Accept all defaults; create the vault if missing."
|
|
116
|
+
),
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Set up AMI on this machine: write the config file and create inbox folders."""
|
|
119
|
+
chosen_path = _ask_for_vault_path(vault_path)
|
|
120
|
+
create_if_missing = _ask_to_create_vault(chosen_path, auto_yes=yes)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
outcome = run_init(
|
|
124
|
+
vault_path=chosen_path,
|
|
125
|
+
config_path=user_config_path(),
|
|
126
|
+
create_vault_if_missing=create_if_missing,
|
|
127
|
+
)
|
|
128
|
+
except FileNotFoundError:
|
|
129
|
+
typer.echo("\n Cancelled — your vault wasn't created.", err=True)
|
|
130
|
+
typer.echo(
|
|
131
|
+
f" When you're ready, run `amie init` again or create {chosen_path} manually.\n",
|
|
132
|
+
err=True,
|
|
133
|
+
)
|
|
134
|
+
raise typer.Exit(code=1) from None
|
|
135
|
+
|
|
136
|
+
ssh = setup_ssh_server(sshd_drop_in=Config.sshd_drop_in_from_env())
|
|
137
|
+
_display_init_outcome(outcome, ssh)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command()
|
|
141
|
+
@friendly
|
|
142
|
+
def watch() -> None:
|
|
143
|
+
"""Watch the inbox folders and normalize incoming notes."""
|
|
144
|
+
import signal
|
|
145
|
+
|
|
146
|
+
from amie.watcher import Watcher
|
|
147
|
+
|
|
148
|
+
cfg = Config.from_env()
|
|
149
|
+
_scaffold_with_notice(cfg)
|
|
150
|
+
watcher = Watcher(cfg)
|
|
151
|
+
|
|
152
|
+
def _stop(signum, frame):
|
|
153
|
+
watcher.stop()
|
|
154
|
+
|
|
155
|
+
signal.signal(signal.SIGINT, _stop)
|
|
156
|
+
signal.signal(signal.SIGTERM, _stop)
|
|
157
|
+
|
|
158
|
+
watcher.start()
|
|
159
|
+
watcher._observer.join()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@app.command()
|
|
163
|
+
@friendly
|
|
164
|
+
def digest() -> None:
|
|
165
|
+
"""Generate the weekly digest."""
|
|
166
|
+
cfg = Config.from_env()
|
|
167
|
+
_scaffold_with_notice(cfg)
|
|
168
|
+
typer.echo("digest: not yet implemented (see docs/amie/11-weekly-digest.md)")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command()
|
|
172
|
+
@friendly
|
|
173
|
+
def ocr(path: str) -> None:
|
|
174
|
+
"""Run OCR on a single image and print the transcript."""
|
|
175
|
+
cfg = Config.from_env()
|
|
176
|
+
_scaffold_with_notice(cfg)
|
|
177
|
+
typer.echo(f"ocr {path}: not yet implemented (see docs/amie/09-photo-ocr.md)")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@app.command()
|
|
181
|
+
@friendly
|
|
182
|
+
def doctor() -> None:
|
|
183
|
+
"""Run all health checks and report status."""
|
|
184
|
+
cfg = Config.from_env()
|
|
185
|
+
results: list[CheckResult] = run_all_checks(cfg)
|
|
186
|
+
for result in results:
|
|
187
|
+
marker = "OK" if result.ok else "FAIL"
|
|
188
|
+
typer.echo(f" [{marker}] {result.name}: {result.message}")
|
|
189
|
+
if any(not r.ok for r in results):
|
|
190
|
+
raise typer.Exit(code=1)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# dev sub-app
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dev_app.command("test")
|
|
199
|
+
def dev_test(no_cov: bool = typer.Option(False, "--no-cov", help="Skip coverage gate.")) -> None:
|
|
200
|
+
"""Run the test suite under uv. Coverage gate is enforced by default."""
|
|
201
|
+
rc = dev.run(dev.build_test_command(no_cov=no_cov))
|
|
202
|
+
raise typer.Exit(code=rc)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dev_app.command("lint")
|
|
206
|
+
def dev_lint(fix: bool = typer.Option(False, "--fix", help="Auto-fix lint issues.")) -> None:
|
|
207
|
+
"""Run ruff check across the project."""
|
|
208
|
+
rc = dev.run(dev.build_lint_command(fix=fix))
|
|
209
|
+
raise typer.Exit(code=rc)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dev_app.command("fmt")
|
|
213
|
+
def dev_fmt() -> None:
|
|
214
|
+
"""Format the codebase with ruff."""
|
|
215
|
+
rc = dev.run(dev.build_fmt_command())
|
|
216
|
+
raise typer.Exit(code=rc)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dev_app.command("sync")
|
|
220
|
+
def dev_sync() -> None:
|
|
221
|
+
"""Sync project dependencies (uv sync)."""
|
|
222
|
+
rc = dev.run(dev.build_sync_command())
|
|
223
|
+
raise typer.Exit(code=rc)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dev_app.command("guard")
|
|
227
|
+
def dev_guard(
|
|
228
|
+
skip_audit: bool = typer.Option(
|
|
229
|
+
False,
|
|
230
|
+
"--skip-audit",
|
|
231
|
+
help="Skip the security-audit step (which makes a network call). Use for fast iteration.",
|
|
232
|
+
),
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Run lint + format-check + tests-with-coverage + security audit + docs check.
|
|
235
|
+
|
|
236
|
+
Run before AND after every code change. With `--skip-audit`, the network
|
|
237
|
+
call is skipped — useful for tight inner loops, but full guard must pass
|
|
238
|
+
before declaring work done.
|
|
239
|
+
|
|
240
|
+
The docs-check step runs automatically when AMIE_DOCS_PATH is set.
|
|
241
|
+
"""
|
|
242
|
+
results = dev.run_guards(skip_audit=skip_audit, docs_dir=Config.docs_path_from_env())
|
|
243
|
+
for result in results:
|
|
244
|
+
marker = "OK" if result.passed else "FAIL"
|
|
245
|
+
typer.echo(f" [{marker}] {result.name}")
|
|
246
|
+
if results and not results[-1].passed:
|
|
247
|
+
raise typer.Exit(code=results[-1].exit_code)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dev_app.command("audit")
|
|
251
|
+
def dev_audit() -> None:
|
|
252
|
+
"""Run a security audit on the project's dependencies (pip-audit)."""
|
|
253
|
+
rc = dev.run(security.build_audit_command())
|
|
254
|
+
raise typer.Exit(code=rc)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dev_app.command("docs")
|
|
258
|
+
@friendly
|
|
259
|
+
def dev_docs(
|
|
260
|
+
docs_dir: str = typer.Option(
|
|
261
|
+
"",
|
|
262
|
+
"--docs-dir",
|
|
263
|
+
help="Path to the docs folder. Defaults to AMIE_DOCS_PATH env var.",
|
|
264
|
+
),
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Check that the docs folder is not stale.
|
|
267
|
+
|
|
268
|
+
Verifies that vault-structure.md mentions every top-level vault section
|
|
269
|
+
and that 14-cli-reference.md mentions every CLI command. Exits non-zero
|
|
270
|
+
and lists gaps if anything is out of date.
|
|
271
|
+
|
|
272
|
+
Set AMIE_DOCS_PATH in your .env or environment to run this automatically
|
|
273
|
+
as part of `amie dev guard`.
|
|
274
|
+
"""
|
|
275
|
+
from amie.docs import run_docs_check
|
|
276
|
+
from amie.vault import VAULT_SKELETON
|
|
277
|
+
|
|
278
|
+
resolved = Path(docs_dir) if docs_dir else Config.docs_path_from_env()
|
|
279
|
+
if resolved is None:
|
|
280
|
+
typer.echo(
|
|
281
|
+
" docs check skipped — set AMIE_DOCS_PATH to enable.\n"
|
|
282
|
+
" Example: AMIE_DOCS_PATH=~/source/ubuntu/docs/amie"
|
|
283
|
+
)
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
commands = _collect_all_commands()
|
|
287
|
+
issues = run_docs_check(resolved, skeleton=VAULT_SKELETON, commands=commands)
|
|
288
|
+
|
|
289
|
+
if not issues:
|
|
290
|
+
typer.echo(f" OK — docs are current ({resolved})")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
typer.echo(f" FAIL — {len(issues)} doc gap(s) found:\n", err=True)
|
|
294
|
+
for issue in issues:
|
|
295
|
+
typer.echo(f" [{issue.doc}] {issue.missing}", err=True)
|
|
296
|
+
raise typer.Exit(code=1)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Private helpers
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _display_init_outcome(
|
|
305
|
+
outcome: InitOutcome,
|
|
306
|
+
ssh: SshSetupResult,
|
|
307
|
+
*,
|
|
308
|
+
file: object = None,
|
|
309
|
+
) -> None:
|
|
310
|
+
vault_state = "created" if outcome.vault_created else "already existed"
|
|
311
|
+
sshd_state = "installed" if ssh.sshd_was_installed else "already present"
|
|
312
|
+
user_state = "created" if ssh.user_was_created else "already present"
|
|
313
|
+
kwargs = {"file": file} if file is not None else {}
|
|
314
|
+
typer.echo("", **kwargs)
|
|
315
|
+
typer.echo(" All set!", **kwargs)
|
|
316
|
+
typer.echo(f" config: {outcome.config_path}", **kwargs)
|
|
317
|
+
typer.echo(f" vault: {outcome.vault_path} ({vault_state})", **kwargs)
|
|
318
|
+
typer.echo(f" vault folders: {outcome.folders_created} created or verified", **kwargs)
|
|
319
|
+
typer.echo(f" openssh-server: {sshd_state}", **kwargs)
|
|
320
|
+
typer.echo(f" amie-sync user: {user_state}", **kwargs)
|
|
321
|
+
typer.echo("\n Next: run `amie doctor` to confirm Ollama and SSH are ready.\n", **kwargs)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _scaffold_with_notice(cfg: Config) -> None:
|
|
325
|
+
"""Create any missing vault folders, printing a notice if any were added."""
|
|
326
|
+
created = vault.scaffold_vault(cfg)
|
|
327
|
+
if created:
|
|
328
|
+
typer.echo(
|
|
329
|
+
f"Notice: created {len(created)} missing vault folder(s)"
|
|
330
|
+
" — run `amie vault status` for details."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _collect_all_commands() -> list[str]:
|
|
335
|
+
"""Return every CLI command name as it appears in the reference doc."""
|
|
336
|
+
|
|
337
|
+
def _name(cmd_info) -> str:
|
|
338
|
+
if cmd_info.name:
|
|
339
|
+
return cmd_info.name
|
|
340
|
+
if cmd_info.callback:
|
|
341
|
+
return cmd_info.callback.__name__.replace("_", "-")
|
|
342
|
+
return ""
|
|
343
|
+
|
|
344
|
+
commands = [f"amie {_name(c)}" for c in app.registered_commands if _name(c)]
|
|
345
|
+
commands += [f"amie dev {_name(c)}" for c in dev_app.registered_commands if _name(c)]
|
|
346
|
+
commands += [f"amie vault {_name(c)}" for c in vault_app.registered_commands if _name(c)]
|
|
347
|
+
return [c for c in commands if c.strip()]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
# vault sub-app
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@vault_app.command("init")
|
|
356
|
+
@friendly
|
|
357
|
+
def vault_init() -> None:
|
|
358
|
+
"""Create the full vault folder structure inside the Obsidian vault."""
|
|
359
|
+
cfg = Config.from_env()
|
|
360
|
+
created = vault.scaffold_vault(cfg)
|
|
361
|
+
if created:
|
|
362
|
+
typer.echo(f"OK — created {len(created)} folders under {cfg.vault_path}")
|
|
363
|
+
else:
|
|
364
|
+
typer.echo(f"OK — all vault folders already present under {cfg.vault_path}")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@vault_app.command("status")
|
|
368
|
+
@friendly
|
|
369
|
+
def vault_status_cmd() -> None:
|
|
370
|
+
"""Report whether AMI's folders exist inside the vault."""
|
|
371
|
+
cfg = Config.from_env()
|
|
372
|
+
status = vault.vault_status(cfg)
|
|
373
|
+
if not status.vault_exists:
|
|
374
|
+
typer.echo(f"vault: not ready — root missing at {cfg.vault_path}")
|
|
375
|
+
raise typer.Exit(code=1)
|
|
376
|
+
if status.missing:
|
|
377
|
+
names = "\n - ".join(p.relative_to(cfg.vault_path).as_posix() for p in status.missing)
|
|
378
|
+
typer.echo(f"vault: not ready — missing dirs:\n - {names}")
|
|
379
|
+
raise typer.Exit(code=1)
|
|
380
|
+
typer.echo(f"vault: ready — all inbox directories present at {cfg.vault_path}")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
# Private helpers (CLI-side prompts)
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _ask_for_vault_path(provided: Path | None) -> Path:
|
|
389
|
+
"""If the user supplied --vault-path, use it. Otherwise prompt with a default."""
|
|
390
|
+
if provided is not None:
|
|
391
|
+
return provided
|
|
392
|
+
default = Path.home() / "Obsidian"
|
|
393
|
+
answer: str = typer.prompt(
|
|
394
|
+
"Where is (or should be) your Obsidian vault?",
|
|
395
|
+
default=str(default),
|
|
396
|
+
)
|
|
397
|
+
return Path(answer).expanduser()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _ask_to_create_vault(vault_path: Path, *, auto_yes: bool) -> bool:
|
|
401
|
+
"""If the vault doesn't exist, ask whether to create it. Default yes."""
|
|
402
|
+
if vault_path.expanduser().is_dir():
|
|
403
|
+
return False
|
|
404
|
+
if auto_yes:
|
|
405
|
+
return True
|
|
406
|
+
return typer.confirm(f" {vault_path} doesn't exist yet. Create it now?", default=True)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
if __name__ == "__main__":
|
|
410
|
+
sys.exit(app())
|