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 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,3 @@
1
+ """AMI — Anonymous Memory Intelligence."""
2
+
3
+ __version__ = "0.0.1"
@@ -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())