nexus-cli 0.6.0__tar.gz → 0.7.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.
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.STATUS +8 -8
- nexus_cli-0.7.0/.claude/guard-session-counts +1 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.github/workflows/homebrew-release.yml +6 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/CLAUDE.md +6 -3
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/PKG-INFO +20 -4
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/README.md +17 -1
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/adhd-quick-start.md +1 -1
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/changelog.md +25 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/index.md +1 -1
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/__init__.py +1 -1
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/cli.py +11 -1
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/pyproject.toml +3 -3
- nexus_cli-0.7.0/tests/test_vault.py +409 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/uv.lock +1 -1
- nexus_cli-0.6.0/tests/test_vault.py +0 -218
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.github/workflows/ci.yml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.github/workflows/docs.yml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.github/workflows/publish.yml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.github/workflows/quality.yml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.github/workflows/release.yml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.github/workflows/test.yml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.gitignore +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/.python-version +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/CI_CD_SUMMARY.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/DOCUMENTATION_UPDATE_SUMMARY.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/FINAL_SUMMARY.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/HOMEBREW_FORMULA_SETUP.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/INSTALLATION_TROUBLESHOOTING.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/ORCHESTRATE-nexus-enhancement.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/PLANNING.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/README_BADGES.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/RELEASE_NOTES_v0.4.0.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/RELEASE_NOTES_v0.5.0.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/RELEASE_NOTES_v0.5.1.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/SESSION_COMPLETE.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/SESSION_COMPLETE.txt +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/SESSION_WRAP_UP.txt +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/TEST_SUMMARY.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/TODO.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/TUTORIAL_GUIDE.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/TUTORIAL_QUICK_REF.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/sessions-2025-12/SESSION_2025-12-25_TUTORIAL_SYSTEM.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/sessions-2025-12/SESSION_COMPLETE_2025-12-25.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/sessions-2025-12/SESSION_SUMMARY.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/sessions-2025-12/TUTORIAL_SYSTEM_COMPLETE.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/testing-utilities/FINAL_SESSION_SUMMARY.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/testing-utilities/Formula-local-test.rb +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/testing-utilities/QUICK_START_TESTING.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/testing-utilities/QUICK_TEST_GUIDE.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/testing-utilities/TESTING_GUIDE.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/testing-utilities/interactive_dogfood.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/archive/testing-utilities/test_local_install.sh +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/config/nexus.example.yaml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/development/architecture.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/development/contributing.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/development/testing.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/getting-started/configuration.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/getting-started/installation.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/getting-started/quickstart.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/guide/knowledge.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/guide/overview.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/guide/research.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/guide/teaching.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/guide/writing.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/reference/api.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/reference/cli.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/reference/mcp.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/reference/refcard.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/specs/SPEC-bibliography-coverage.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/specs/SPEC-cli-coverage.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/specs/SPEC-courses-coverage.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/specs/SPEC-nexus-doctor.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/tutorials/first-steps.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/tutorials/graph-viz.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/tutorials/tutorial-guide.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/tutorials/tutorial-quick-ref.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/tutorials/tutorial-system.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/tutorials/vault-setup.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/tutorials/zotero.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/docs/workflows/index.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/install.sh +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/main.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/mkdocs.yml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/data/vault-spec.yaml +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/integrations/__init__.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/knowledge/__init__.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/knowledge/search.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/knowledge/vault.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/research/__init__.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/research/pdf.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/research/zotero.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/teaching/__init__.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/teaching/courses.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/teaching/quarto.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/utils/__init__.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/utils/config.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/utils/help_text.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/utils/tutorial.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/utils/vault_spec.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/writing/__init__.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/writing/bibliography.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/nexus/writing/manuscript.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/output.json +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/.claude-plugin/plugin.json +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/README.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/commands/check.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/commands/cite.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/commands/dashboard.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/commands/manuscripts.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/commands/search.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/skills/integration-patterns/skill.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/skills/knowledge/vault-operations/skill.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/skills/research/zotero-integration/skill.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/skills/teaching/course-management/skill.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/plugin/skills/writing/manuscript-management/skill.md +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/__init__.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/conftest.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_bibliography.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_cli.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_cli_commands.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_cli_coverage.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_cli_integration.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_cli_research_commands.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_cli_vault_commands.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_cli_writing_commands.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_config.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_courses.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_dogfooding.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_edge_cases.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_graph_export.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_help_text.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_manuscript.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_manuscript_batch.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_pdf.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_pdf_extractor.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_quarto.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_quarto_manager.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_search.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_tutorial.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_validation.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_vault_spec.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_zotero.py +0 -0
- {nexus_cli-0.6.0 → nexus_cli-0.7.0}/tests/test_zotero_client.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
status:
|
|
2
|
-
priority:
|
|
3
|
-
progress:
|
|
4
|
-
next:
|
|
1
|
+
status: deprecated
|
|
2
|
+
priority: 0
|
|
3
|
+
progress: 100
|
|
4
|
+
next: RETIRED (2026-06-21) — absorbed into obsidian-cli-ops (obs) per RFC v2 D1=Option A. Deprecation banner added. Phase 5 remaining: ship v0.7.0 to PyPI with deprecation banner, retire Homebrew formula, archive GitHub repo.
|
|
5
5
|
type: dev
|
|
6
|
-
checkpoint: 2026-06-03 — v0.6.
|
|
7
|
-
version: 0.6.
|
|
8
|
-
release_date: 2026-06-
|
|
9
|
-
release_url: https://github.com/Data-Wise/nexus-cli/releases/tag/v0.
|
|
6
|
+
checkpoint: 2026-06-03 — v0.6.1 FULLY SHIPPED (patch: vault.py 78%→98.14%, +34 tests). PR #10 dev→main merged; tagged v0.6.1; PyPI 0.6.1 published (OIDC); GitHub release live; Homebrew formula manually pushed (bc186f9) + brew audit clean; CI green all platforms. Prior: v0.6.0 shipped same day.
|
|
7
|
+
version: 0.6.1
|
|
8
|
+
release_date: 2026-06-03
|
|
9
|
+
release_url: https://github.com/Data-Wise/nexus-cli/releases/tag/v0.6.1
|
|
10
10
|
docs_url: https://data-wise.github.io/nexus-cli/
|
|
11
11
|
planning_doc: ORCHESTRATE-nexus-enhancement.md
|
|
12
12
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
edit_existing
|
|
@@ -59,4 +59,10 @@ jobs:
|
|
|
59
59
|
source_type: pypi
|
|
60
60
|
auto_merge: ${{ github.event.inputs.auto_merge == 'true' || github.event_name == 'workflow_dispatch' }}
|
|
61
61
|
secrets:
|
|
62
|
+
# Prefer GitHub App auth (short-lived, scoped, no expiry to rotate).
|
|
63
|
+
# The reusable workflow mints an App token and only falls back to
|
|
64
|
+
# tap_token (an expiring PAT) when the App secrets are absent.
|
|
65
|
+
# Matches craft's pattern — see homebrew-tap update-formula.yml.
|
|
66
|
+
app_id: ${{ secrets.APP_ID }}
|
|
67
|
+
app_private_key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
62
68
|
tap_token: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
|
@@ -13,13 +13,16 @@ Nexus is a **knowledge workflow CLI** for academic researchers. It provides data
|
|
|
13
13
|
- Teaching materials
|
|
14
14
|
- Manuscripts
|
|
15
15
|
|
|
16
|
-
**Current Version**: 0.
|
|
16
|
+
**Current Version**: 0.7.0 — **DEPRECATED**
|
|
17
|
+
|
|
18
|
+
> [!warning] nexus-cli is retired
|
|
19
|
+
> All functionality has been absorbed into `obsidian-cli-ops` (`obs`) as of Phase 1 (PR #37, 2026-06-21). This is the final release (v0.7.0). See the [migration guide](https://data-wise.github.io/obsidian-cli-ops/migration/) for command mappings. Use `obs research` and `obs config` going forward.
|
|
17
20
|
|
|
18
21
|
## Quick Start for Claude
|
|
19
22
|
|
|
20
23
|
```bash
|
|
21
24
|
# Check installation
|
|
22
|
-
nexus --version # Should show 0.
|
|
25
|
+
nexus --version # Should show 0.7.0 (final deprecated release)
|
|
23
26
|
nexus doctor # Health check
|
|
24
27
|
|
|
25
28
|
# Interactive learning system
|
|
@@ -463,7 +466,7 @@ echo $PATH | grep ".local/bin"
|
|
|
463
466
|
✅ Full type safety (0 mypy errors across 18 source files)
|
|
464
467
|
|
|
465
468
|
### Key Metrics
|
|
466
|
-
- **Version**: 0.
|
|
469
|
+
- **Version**: 0.7.0 (DEPRECATED — final release)
|
|
467
470
|
- **Tests**: 719 passing, 1 skipped (87.5% coverage)
|
|
468
471
|
- **Mypy**: 0 errors across 20 source files
|
|
469
472
|
- **Commands**: 50+ across 4 domains
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nexus-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: DEPRECATED — absorbed into obsidian-cli-ops (obs). See https://data-wise.github.io/obsidian-cli-ops/migration/
|
|
5
5
|
Project-URL: Homepage, https://github.com/Data-Wise/nexus-cli
|
|
6
6
|
Project-URL: Documentation, https://data-wise.github.io/nexus-cli
|
|
7
7
|
Project-URL: Repository, https://github.com/Data-Wise/nexus-cli
|
|
8
8
|
Author-email: DT <dt@example.com>
|
|
9
9
|
License: MIT
|
|
10
10
|
Keywords: claude,cli,knowledge-management,obsidian,research,zotero
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 7 - Inactive
|
|
12
12
|
Classifier: Environment :: Console
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -39,7 +39,23 @@ Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
|
|
|
39
39
|
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == 'docs'
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
|
|
42
|
-
# Nexus CLI
|
|
42
|
+
# Nexus CLI — DEPRECATED
|
|
43
|
+
|
|
44
|
+
> **⚠️ nexus-cli has been absorbed into [obsidian-cli-ops](https://github.com/Data-Wise/obsidian-cli-ops) (`obs`).**
|
|
45
|
+
>
|
|
46
|
+
> All research, teaching, writing, and vault commands are now available under `obs research …`.
|
|
47
|
+
> This package will no longer receive updates. Please migrate:
|
|
48
|
+
>
|
|
49
|
+
> ```sh
|
|
50
|
+
> brew upgrade data-wise/tap/obsidian-cli-ops
|
|
51
|
+
> # obs research zotero search "query"
|
|
52
|
+
> # obs research manuscript list
|
|
53
|
+
> # obs research course list
|
|
54
|
+
> ```
|
|
55
|
+
>
|
|
56
|
+
> See the [migration guide](https://data-wise.github.io/obsidian-cli-ops/migration/) for the full command mapping.
|
|
57
|
+
|
|
58
|
+
---
|
|
43
59
|
|
|
44
60
|
[](https://github.com/Data-Wise/nexus-cli/actions/workflows/ci.yml)
|
|
45
61
|
[](https://data-wise.github.io/nexus-cli)
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
# Nexus CLI
|
|
1
|
+
# Nexus CLI — DEPRECATED
|
|
2
|
+
|
|
3
|
+
> **⚠️ nexus-cli has been absorbed into [obsidian-cli-ops](https://github.com/Data-Wise/obsidian-cli-ops) (`obs`).**
|
|
4
|
+
>
|
|
5
|
+
> All research, teaching, writing, and vault commands are now available under `obs research …`.
|
|
6
|
+
> This package will no longer receive updates. Please migrate:
|
|
7
|
+
>
|
|
8
|
+
> ```sh
|
|
9
|
+
> brew upgrade data-wise/tap/obsidian-cli-ops
|
|
10
|
+
> # obs research zotero search "query"
|
|
11
|
+
> # obs research manuscript list
|
|
12
|
+
> # obs research course list
|
|
13
|
+
> ```
|
|
14
|
+
>
|
|
15
|
+
> See the [migration guide](https://data-wise.github.io/obsidian-cli-ops/migration/) for the full command mapping.
|
|
16
|
+
|
|
17
|
+
---
|
|
2
18
|
|
|
3
19
|
[](https://github.com/Data-Wise/nexus-cli/actions/workflows/ci.yml)
|
|
4
20
|
[](https://data-wise.github.io/nexus-cli)
|
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## [0.7.0] - 2026-06-21
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **DEPRECATED**: nexus-cli is now inactive. All functionality has been absorbed into
|
|
6
|
+
[obsidian-cli-ops (obs)](https://data-wise.github.io/obsidian-cli-ops/). See the
|
|
7
|
+
[migration guide](https://data-wise.github.io/obsidian-cli-ops/migration/) for the
|
|
8
|
+
full command mapping.
|
|
9
|
+
- Deprecation banner added to all `nexus` invocations.
|
|
10
|
+
- `Development Status :: 7 - Inactive` PyPI classifier applied.
|
|
11
|
+
- This is the final release. The package will remain on PyPI for existing installs.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [0.6.1] - 2026-06-03
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Test coverage**: `vault.py` raised from 78% → **98.14%** (+34 tests),
|
|
19
|
+
meeting the last outstanding v0.6.0 coverage target. New tests cover the
|
|
20
|
+
ripgrep search fallback (rg-missing, timeout, malformed JSON), `search_files`,
|
|
21
|
+
template loading/creation, daily notes, invalid-YAML frontmatter handling,
|
|
22
|
+
missing-vault guards, and the tag-graph node/edge paths.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
1
26
|
## [0.6.0] - 2026-06-03
|
|
2
27
|
|
|
3
28
|
### Added
|
|
@@ -155,7 +155,7 @@ MIT License - see [LICENSE](https://github.com/Data-Wise/nexus-cli/blob/main/LIC
|
|
|
155
155
|
|
|
156
156
|
## Project Stats
|
|
157
157
|
|
|
158
|
-
**Current Version**: 0.
|
|
158
|
+
**Current Version**: 0.7.0 (DEPRECATED — see [migration guide](https://data-wise.github.io/obsidian-cli-ops/migration/))
|
|
159
159
|
|
|
160
160
|
- **Test Coverage**: 87.5% (719 tests passing)
|
|
161
161
|
- **Type Safety**: 0 mypy errors across all 20 source files ✅
|
|
@@ -99,7 +99,17 @@ def main(
|
|
|
99
99
|
|
|
100
100
|
[dim]Claude is the brain, Nexus is the body.[/]
|
|
101
101
|
"""
|
|
102
|
-
|
|
102
|
+
console.print(
|
|
103
|
+
Panel(
|
|
104
|
+
"[bold yellow]⚠ nexus-cli is deprecated[/]\n\n"
|
|
105
|
+
"All research, teaching, writing, and vault commands have been absorbed\n"
|
|
106
|
+
"into [bold cyan]obs[/] (obsidian-cli-ops) under [cyan]obs research …[/].\n\n"
|
|
107
|
+
"Migration: [dim]brew upgrade data-wise/tap/obsidian-cli-ops[/]\n"
|
|
108
|
+
"Guide: [dim]https://data-wise.github.io/obsidian-cli-ops/migration/[/]",
|
|
109
|
+
title="[bold red]Deprecated — use obs instead[/]",
|
|
110
|
+
border_style="yellow",
|
|
111
|
+
)
|
|
112
|
+
)
|
|
103
113
|
|
|
104
114
|
|
|
105
115
|
@app.command()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nexus-cli"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.7.0"
|
|
4
|
+
description = "DEPRECATED — absorbed into obsidian-cli-ops (obs). See https://data-wise.github.io/obsidian-cli-ops/migration/"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
license = { text = "MIT" }
|
|
@@ -10,7 +10,7 @@ authors = [
|
|
|
10
10
|
]
|
|
11
11
|
keywords = ["cli", "research", "knowledge-management", "zotero", "obsidian", "claude"]
|
|
12
12
|
classifiers = [
|
|
13
|
-
"Development Status ::
|
|
13
|
+
"Development Status :: 7 - Inactive",
|
|
14
14
|
"Environment :: Console",
|
|
15
15
|
"Intended Audience :: Science/Research",
|
|
16
16
|
"License :: OSI Approved :: MIT License",
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Tests for vault management module."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from datetime import date
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from nexus.knowledge.vault import VaultManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestVaultManager:
|
|
13
|
+
"""Tests for VaultManager class."""
|
|
14
|
+
|
|
15
|
+
def test_vault_exists(self, sample_vault):
|
|
16
|
+
"""Test that vault existence check works."""
|
|
17
|
+
manager = VaultManager(sample_vault)
|
|
18
|
+
assert manager.exists() is True
|
|
19
|
+
|
|
20
|
+
def test_vault_not_exists(self, temp_dir):
|
|
21
|
+
"""Test non-existent vault."""
|
|
22
|
+
manager = VaultManager(temp_dir / "nonexistent")
|
|
23
|
+
assert manager.exists() is False
|
|
24
|
+
|
|
25
|
+
def test_note_count(self, sample_vault):
|
|
26
|
+
"""Test counting notes in vault."""
|
|
27
|
+
manager = VaultManager(sample_vault)
|
|
28
|
+
count = manager.note_count()
|
|
29
|
+
|
|
30
|
+
# Should find the sample notes
|
|
31
|
+
assert count >= 2
|
|
32
|
+
|
|
33
|
+
def test_search_notes(self, sample_vault):
|
|
34
|
+
"""Test searching notes."""
|
|
35
|
+
manager = VaultManager(sample_vault)
|
|
36
|
+
|
|
37
|
+
# Search for mediation
|
|
38
|
+
results = manager.search("mediation")
|
|
39
|
+
assert len(results) >= 1
|
|
40
|
+
|
|
41
|
+
# Search for something not in notes
|
|
42
|
+
results = manager.search("xyznonexistent123")
|
|
43
|
+
assert len(results) == 0
|
|
44
|
+
|
|
45
|
+
def test_recent_notes(self, sample_vault):
|
|
46
|
+
"""Test getting recent notes."""
|
|
47
|
+
manager = VaultManager(sample_vault)
|
|
48
|
+
recent = manager.recent(limit=5)
|
|
49
|
+
|
|
50
|
+
assert len(recent) >= 2
|
|
51
|
+
assert len(recent) <= 5
|
|
52
|
+
|
|
53
|
+
def test_read_note(self, sample_vault):
|
|
54
|
+
"""Test reading a note."""
|
|
55
|
+
manager = VaultManager(sample_vault)
|
|
56
|
+
note = manager.read("10-PROJECTS/test-project.md")
|
|
57
|
+
|
|
58
|
+
assert note is not None
|
|
59
|
+
assert "mediation analysis" in note.content
|
|
60
|
+
|
|
61
|
+
def test_read_nonexistent_note(self, sample_vault):
|
|
62
|
+
"""Test reading non-existent note raises error."""
|
|
63
|
+
manager = VaultManager(sample_vault)
|
|
64
|
+
|
|
65
|
+
with pytest.raises(FileNotFoundError):
|
|
66
|
+
manager.read("nonexistent.md")
|
|
67
|
+
|
|
68
|
+
def test_backlinks(self, sample_vault):
|
|
69
|
+
"""Test finding backlinks."""
|
|
70
|
+
manager = VaultManager(sample_vault)
|
|
71
|
+
backlinks = manager.backlinks("10-PROJECTS/test-project.md")
|
|
72
|
+
|
|
73
|
+
# causal-inference.md links to test-project
|
|
74
|
+
assert len(backlinks) >= 1
|
|
75
|
+
|
|
76
|
+
def test_orphans(self, sample_vault):
|
|
77
|
+
"""Test finding orphan notes."""
|
|
78
|
+
manager = VaultManager(sample_vault)
|
|
79
|
+
|
|
80
|
+
# Create an orphan note
|
|
81
|
+
orphan = sample_vault / "orphan-note.md"
|
|
82
|
+
orphan.write_text("# Orphan\n\nThis note has no links.")
|
|
83
|
+
|
|
84
|
+
orphans = manager.orphans()
|
|
85
|
+
|
|
86
|
+
assert "orphan-note.md" in orphans
|
|
87
|
+
|
|
88
|
+
def test_graph_generation(self, sample_vault):
|
|
89
|
+
"""Test generating graph data."""
|
|
90
|
+
manager = VaultManager(sample_vault)
|
|
91
|
+
graph = manager.graph()
|
|
92
|
+
|
|
93
|
+
assert "nodes" in graph
|
|
94
|
+
assert "edges" in graph
|
|
95
|
+
assert len(graph["nodes"]) >= 2
|
|
96
|
+
|
|
97
|
+
# Check node structure
|
|
98
|
+
node = graph["nodes"][0]
|
|
99
|
+
assert "id" in node
|
|
100
|
+
assert "label" in node
|
|
101
|
+
assert "path" in node
|
|
102
|
+
assert "connections" in node
|
|
103
|
+
|
|
104
|
+
def test_graph_with_limit(self, sample_vault):
|
|
105
|
+
"""Test graph generation with node limit."""
|
|
106
|
+
manager = VaultManager(sample_vault)
|
|
107
|
+
graph = manager.graph(limit=1)
|
|
108
|
+
|
|
109
|
+
assert len(graph["nodes"]) <= 1
|
|
110
|
+
|
|
111
|
+
def test_graph_with_tags(self, sample_vault):
|
|
112
|
+
"""Test graph generation including tags."""
|
|
113
|
+
manager = VaultManager(sample_vault)
|
|
114
|
+
graph = manager.graph(include_tags=True)
|
|
115
|
+
|
|
116
|
+
# May or may not have tag nodes depending on sample data
|
|
117
|
+
tag_nodes = [n for n in graph["nodes"] if n.get("type") == "tag"]
|
|
118
|
+
assert isinstance(tag_nodes, list) # Just verify structure
|
|
119
|
+
|
|
120
|
+
def test_graph_stats(self, sample_vault):
|
|
121
|
+
"""Test graph statistics calculation."""
|
|
122
|
+
manager = VaultManager(sample_vault)
|
|
123
|
+
stats = manager.graph_stats()
|
|
124
|
+
|
|
125
|
+
assert "total_notes" in stats
|
|
126
|
+
assert "total_connections" in stats
|
|
127
|
+
assert "avg_connections" in stats
|
|
128
|
+
assert "most_connected" in stats
|
|
129
|
+
assert "density" in stats
|
|
130
|
+
|
|
131
|
+
assert stats["total_notes"] >= 2
|
|
132
|
+
assert isinstance(stats["avg_connections"], float)
|
|
133
|
+
assert isinstance(stats["most_connected"], list)
|
|
134
|
+
|
|
135
|
+
def test_write_note(self, sample_vault):
|
|
136
|
+
"""Test writing a new note."""
|
|
137
|
+
manager = VaultManager(sample_vault)
|
|
138
|
+
|
|
139
|
+
content = "# New Note\n\nThis is test content."
|
|
140
|
+
result = manager.write("test-write.md", content)
|
|
141
|
+
|
|
142
|
+
assert result is not None
|
|
143
|
+
assert (sample_vault / "test-write.md").exists()
|
|
144
|
+
|
|
145
|
+
# Verify content
|
|
146
|
+
note = manager.read("test-write.md")
|
|
147
|
+
assert "test content" in note.content
|
|
148
|
+
|
|
149
|
+
def test_write_to_subdirectory(self, sample_vault):
|
|
150
|
+
"""Test writing note to subdirectory."""
|
|
151
|
+
manager = VaultManager(sample_vault)
|
|
152
|
+
|
|
153
|
+
content = "# Nested Note\n\nIn a subdirectory."
|
|
154
|
+
result = manager.write("10-PROJECTS/nested-note.md", content)
|
|
155
|
+
|
|
156
|
+
assert result is not None
|
|
157
|
+
assert (sample_vault / "10-PROJECTS" / "nested-note.md").exists()
|
|
158
|
+
|
|
159
|
+
def test_search_with_limit(self, sample_vault):
|
|
160
|
+
"""Test search with result limit."""
|
|
161
|
+
manager = VaultManager(sample_vault)
|
|
162
|
+
|
|
163
|
+
results = manager.search("", limit=1)
|
|
164
|
+
assert len(results) <= 1
|
|
165
|
+
|
|
166
|
+
def test_search_in_frontmatter(self, sample_vault):
|
|
167
|
+
"""Test searching in frontmatter."""
|
|
168
|
+
manager = VaultManager(sample_vault)
|
|
169
|
+
|
|
170
|
+
results = manager.search("active")
|
|
171
|
+
# Should find notes with status: active in frontmatter
|
|
172
|
+
assert len(results) >= 1
|
|
173
|
+
|
|
174
|
+
def test_parse_note_with_links(self, sample_vault):
|
|
175
|
+
"""Test parsing note links."""
|
|
176
|
+
manager = VaultManager(sample_vault)
|
|
177
|
+
note = manager.read("20-AREAS/causal-inference.md")
|
|
178
|
+
|
|
179
|
+
assert len(note.links) >= 1
|
|
180
|
+
assert "test-project" in note.links
|
|
181
|
+
|
|
182
|
+
def test_parse_note_with_tags(self, sample_vault):
|
|
183
|
+
"""Test parsing note tags."""
|
|
184
|
+
manager = VaultManager(sample_vault)
|
|
185
|
+
note = manager.read("10-PROJECTS/test-project.md")
|
|
186
|
+
|
|
187
|
+
# Tags may or may not be present depending on content
|
|
188
|
+
assert isinstance(note.tags, list)
|
|
189
|
+
|
|
190
|
+
def test_parse_frontmatter(self, sample_vault):
|
|
191
|
+
"""Test frontmatter parsing."""
|
|
192
|
+
manager = VaultManager(sample_vault)
|
|
193
|
+
note = manager.read("10-PROJECTS/test-project.md")
|
|
194
|
+
|
|
195
|
+
assert "type" in note.frontmatter
|
|
196
|
+
assert note.frontmatter["type"] == "project"
|
|
197
|
+
|
|
198
|
+
def test_backlinks_empty(self, sample_vault):
|
|
199
|
+
"""Test backlinks for note with no incoming links."""
|
|
200
|
+
manager = VaultManager(sample_vault)
|
|
201
|
+
|
|
202
|
+
# Create isolated note
|
|
203
|
+
isolated = sample_vault / "isolated.md"
|
|
204
|
+
isolated.write_text("# Isolated\n\nNo one links here.")
|
|
205
|
+
|
|
206
|
+
backlinks = manager.backlinks("isolated.md")
|
|
207
|
+
assert len(backlinks) == 0
|
|
208
|
+
|
|
209
|
+
def test_recent_notes_excludes_system(self, sample_vault):
|
|
210
|
+
"""Test that recent notes excludes system folders."""
|
|
211
|
+
manager = VaultManager(sample_vault)
|
|
212
|
+
|
|
213
|
+
# Create a system note
|
|
214
|
+
system_dir = sample_vault / "_SYSTEM"
|
|
215
|
+
system_dir.mkdir(exist_ok=True)
|
|
216
|
+
(system_dir / "config.md").write_text("# System\n\nConfig file.")
|
|
217
|
+
|
|
218
|
+
recent = manager.recent(limit=10)
|
|
219
|
+
|
|
220
|
+
# Should not include _SYSTEM files
|
|
221
|
+
assert not any("_SYSTEM" in path for path in recent)
|
|
222
|
+
assert not any(".obsidian" in path for path in recent)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class TestVaultMissing:
|
|
226
|
+
"""Operations on a non-existent vault should degrade gracefully."""
|
|
227
|
+
|
|
228
|
+
@pytest.fixture
|
|
229
|
+
def missing(self, temp_dir):
|
|
230
|
+
return VaultManager(temp_dir / "no-such-vault")
|
|
231
|
+
|
|
232
|
+
def test_note_count_zero(self, missing):
|
|
233
|
+
assert missing.note_count() == 0
|
|
234
|
+
|
|
235
|
+
def test_search_empty(self, missing):
|
|
236
|
+
assert missing.search("anything") == []
|
|
237
|
+
|
|
238
|
+
def test_search_files_empty(self, missing):
|
|
239
|
+
assert missing.search_files("anything") == []
|
|
240
|
+
|
|
241
|
+
def test_recent_empty(self, missing):
|
|
242
|
+
assert missing.recent() == []
|
|
243
|
+
|
|
244
|
+
def test_orphans_empty(self, missing):
|
|
245
|
+
assert missing.orphans() == []
|
|
246
|
+
|
|
247
|
+
def test_graph_empty(self, missing):
|
|
248
|
+
assert missing.graph() == {"nodes": [], "edges": []}
|
|
249
|
+
|
|
250
|
+
def test_graph_stats_empty(self, missing):
|
|
251
|
+
stats = missing.graph_stats()
|
|
252
|
+
assert stats["total_notes"] == 0
|
|
253
|
+
assert stats["total_connections"] == 0
|
|
254
|
+
assert stats["most_connected"] == []
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TestVaultSearchFallback:
|
|
258
|
+
"""Search fallback and error handling when ripgrep misbehaves."""
|
|
259
|
+
|
|
260
|
+
def test_fallback_search_direct(self, sample_vault):
|
|
261
|
+
manager = VaultManager(sample_vault)
|
|
262
|
+
results = manager._fallback_search("mediation", limit=10)
|
|
263
|
+
assert len(results) >= 1
|
|
264
|
+
assert any("mediation" in r.content.lower() for r in results)
|
|
265
|
+
|
|
266
|
+
def test_fallback_search_respects_limit(self, sample_vault):
|
|
267
|
+
manager = VaultManager(sample_vault)
|
|
268
|
+
results = manager._fallback_search("e", limit=2)
|
|
269
|
+
assert len(results) <= 2
|
|
270
|
+
|
|
271
|
+
def test_search_uses_fallback_when_rg_missing(self, sample_vault):
|
|
272
|
+
manager = VaultManager(sample_vault)
|
|
273
|
+
with patch("subprocess.run", side_effect=FileNotFoundError):
|
|
274
|
+
results = manager.search("mediation")
|
|
275
|
+
assert len(results) >= 1
|
|
276
|
+
|
|
277
|
+
def test_search_returns_empty_on_timeout(self, sample_vault):
|
|
278
|
+
manager = VaultManager(sample_vault)
|
|
279
|
+
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="rg", timeout=30)):
|
|
280
|
+
assert manager.search("mediation") == []
|
|
281
|
+
|
|
282
|
+
def test_search_skips_malformed_json(self, sample_vault):
|
|
283
|
+
manager = VaultManager(sample_vault)
|
|
284
|
+
fake = subprocess.CompletedProcess(
|
|
285
|
+
args=["rg"], returncode=0, stdout='not json\n\n{"type":"other"}\n', stderr=""
|
|
286
|
+
)
|
|
287
|
+
with patch("subprocess.run", return_value=fake):
|
|
288
|
+
# Malformed/non-match lines are skipped, yielding no results
|
|
289
|
+
assert manager.search("mediation") == []
|
|
290
|
+
|
|
291
|
+
def test_search_files_matches_name(self, sample_vault):
|
|
292
|
+
manager = VaultManager(sample_vault)
|
|
293
|
+
matches = manager.search_files("causal")
|
|
294
|
+
assert any("causal-inference" in m for m in matches)
|
|
295
|
+
|
|
296
|
+
def test_search_files_respects_limit(self, sample_vault):
|
|
297
|
+
manager = VaultManager(sample_vault)
|
|
298
|
+
matches = manager.search_files(r"\.md", limit=1)
|
|
299
|
+
assert len(matches) <= 1
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TestVaultFrontmatterParsing:
|
|
303
|
+
"""Edge cases in frontmatter parsing."""
|
|
304
|
+
|
|
305
|
+
def test_read_invalid_yaml_frontmatter(self, sample_vault):
|
|
306
|
+
manager = VaultManager(sample_vault)
|
|
307
|
+
# Unparseable YAML between --- fences: frontmatter falls back to {}
|
|
308
|
+
(sample_vault / "broken-fm.md").write_text("---\n: : bad: [unclosed\n---\n\n# Body\n")
|
|
309
|
+
note = manager.read("broken-fm.md")
|
|
310
|
+
assert note.frontmatter == {}
|
|
311
|
+
assert "Body" in note.content
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class TestVaultTemplates:
|
|
315
|
+
"""Template loading and note creation from templates."""
|
|
316
|
+
|
|
317
|
+
def test_list_templates_empty_without_dir(self, sample_vault):
|
|
318
|
+
manager = VaultManager(sample_vault)
|
|
319
|
+
assert manager.list_templates() == []
|
|
320
|
+
|
|
321
|
+
def test_list_templates_strips_tpl_prefix(self, sample_vault):
|
|
322
|
+
manager = VaultManager(sample_vault)
|
|
323
|
+
manager.templates_path.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
(manager.templates_path / "tpl-meeting.md").write_text("# Meeting")
|
|
325
|
+
(manager.templates_path / "note.md").write_text("# Note")
|
|
326
|
+
templates = manager.list_templates()
|
|
327
|
+
assert "meeting" in templates # tpl- stripped
|
|
328
|
+
assert "note" in templates
|
|
329
|
+
|
|
330
|
+
def test_template_creates_note_with_substitution(self, sample_vault):
|
|
331
|
+
manager = VaultManager(sample_vault)
|
|
332
|
+
manager.templates_path.mkdir(parents=True, exist_ok=True)
|
|
333
|
+
(manager.templates_path / "lit.md").write_text("# {{title}}\n\nBy {{author}} on {{date}}")
|
|
334
|
+
out = manager.template("lit", "30-RESOURCES/new-lit.md", variables={"title": "Paper", "author": "Smith"})
|
|
335
|
+
text = out.read_text()
|
|
336
|
+
assert "# Paper" in text
|
|
337
|
+
assert "By Smith" in text
|
|
338
|
+
assert date.today().isoformat() in text
|
|
339
|
+
|
|
340
|
+
def test_template_missing_raises(self, sample_vault):
|
|
341
|
+
manager = VaultManager(sample_vault)
|
|
342
|
+
with pytest.raises(FileNotFoundError):
|
|
343
|
+
manager.template("does-not-exist", "dest.md")
|
|
344
|
+
|
|
345
|
+
def test_load_template_via_alt_path(self, sample_vault):
|
|
346
|
+
manager = VaultManager(sample_vault)
|
|
347
|
+
# Place template in an alternate location (vault/templates/), not _SYSTEM/templates
|
|
348
|
+
alt = sample_vault / "templates"
|
|
349
|
+
alt.mkdir(parents=True, exist_ok=True)
|
|
350
|
+
(alt / "alt-tpl.md").write_text("# Alt template body")
|
|
351
|
+
loaded = manager._load_template("alt-tpl")
|
|
352
|
+
assert loaded is not None
|
|
353
|
+
assert "Alt template body" in loaded
|
|
354
|
+
|
|
355
|
+
def test_daily_note_uses_template(self, sample_vault):
|
|
356
|
+
manager = VaultManager(sample_vault)
|
|
357
|
+
manager.templates_path.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
(manager.templates_path / "daily.md").write_text("# {{date}}\n\nWeekday: {{date:dddd}}")
|
|
359
|
+
target = date(2026, 6, 3)
|
|
360
|
+
out = manager.daily(target)
|
|
361
|
+
text = out.read_text()
|
|
362
|
+
assert "2026-06-03" in text
|
|
363
|
+
assert "Wednesday" in text # 2026-06-03 is a Wednesday
|
|
364
|
+
|
|
365
|
+
def test_daily_note_default_content(self, sample_vault):
|
|
366
|
+
manager = VaultManager(sample_vault)
|
|
367
|
+
target = date(2026, 6, 4)
|
|
368
|
+
out = manager.daily(target)
|
|
369
|
+
text = out.read_text()
|
|
370
|
+
assert "2026-06-04" in text
|
|
371
|
+
assert "Tasks" in text
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class TestVaultTagGraph:
|
|
375
|
+
"""Graph generation with inline #tags (tag nodes and tag edges)."""
|
|
376
|
+
|
|
377
|
+
@pytest.fixture
|
|
378
|
+
def tagged_vault(self, sample_vault):
|
|
379
|
+
# Two notes sharing an inline #stats tag so the tag node qualifies (>1 connection)
|
|
380
|
+
(sample_vault / "10-PROJECTS" / "a.md").write_text("# A\n\nAbout #stats and #causal methods.\n")
|
|
381
|
+
(sample_vault / "20-AREAS" / "b.md").write_text("# B\n\nMore #stats here, links [[a]].\n")
|
|
382
|
+
return sample_vault
|
|
383
|
+
|
|
384
|
+
def test_graph_includes_tag_nodes(self, tagged_vault):
|
|
385
|
+
manager = VaultManager(tagged_vault)
|
|
386
|
+
graph = manager.graph(include_tags=True)
|
|
387
|
+
tag_nodes = [n for n in graph["nodes"] if n.get("type") == "tag"]
|
|
388
|
+
# #stats appears in 2 notes -> should be a tag node
|
|
389
|
+
assert any(n["id"] == "tag:stats" for n in tag_nodes)
|
|
390
|
+
|
|
391
|
+
def test_graph_includes_tag_edges(self, tagged_vault):
|
|
392
|
+
manager = VaultManager(tagged_vault)
|
|
393
|
+
graph = manager.graph(include_tags=True)
|
|
394
|
+
tag_edges = [e for e in graph["edges"] if e.get("type") == "tag"]
|
|
395
|
+
assert any(e["target"] == "tag:stats" for e in tag_edges)
|
|
396
|
+
|
|
397
|
+
def test_graph_stats_counts_tags(self, tagged_vault):
|
|
398
|
+
manager = VaultManager(tagged_vault)
|
|
399
|
+
stats = manager.graph_stats()
|
|
400
|
+
assert stats["total_notes"] >= 2
|
|
401
|
+
assert "total_tags" in stats
|
|
402
|
+
|
|
403
|
+
def test_graph_skips_system_folders(self, sample_vault):
|
|
404
|
+
manager = VaultManager(sample_vault)
|
|
405
|
+
sys_dir = sample_vault / "_SYSTEM"
|
|
406
|
+
sys_dir.mkdir(exist_ok=True)
|
|
407
|
+
(sys_dir / "secret.md").write_text("# Secret\n\nShould not appear.")
|
|
408
|
+
node_ids = {n["id"] for n in manager.graph()["nodes"]}
|
|
409
|
+
assert "secret" not in node_ids
|