obsidian-knowledge 3.26.0__tar.gz → 3.26.2__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.
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude-plugin/marketplace.json +1 -1
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude-plugin/plugin.json +1 -1
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.codex-plugin/plugin.json +1 -1
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/PKG-INFO +1 -1
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/configuration.md +6 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/primer.py +4 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/vault_files.py +3 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/.codex-plugin/plugin.json +1 -1
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/obsidian-knowledge/SKILL.md +5 -2
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/remember-conversations/SKILL.md +2 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2/plugins/obsidian-knowledge}/skills/vault-organizer/SKILL.md +24 -10
- obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/edit-index.py +180 -0
- obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/find-open-questions.py +186 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/broken-links.md +23 -0
- obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/lib/index-format.md +104 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/rename-files.md +4 -2
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/state-files.md +3 -0
- obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/link_recovery_models.py +97 -0
- obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/organizer_io.py +72 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/recover-unresolved-links.py +90 -121
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/vault-audit.py +40 -16
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/pyproject.toml +1 -1
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/obsidian-knowledge/SKILL.md +5 -2
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/remember-conversations/SKILL.md +2 -0
- {obsidian_knowledge-3.26.0/plugins/obsidian-knowledge → obsidian_knowledge-3.26.2}/skills/vault-organizer/SKILL.md +24 -10
- obsidian_knowledge-3.26.2/skills/vault-organizer/edit-index.py +180 -0
- obsidian_knowledge-3.26.2/skills/vault-organizer/find-open-questions.py +186 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/broken-links.md +23 -0
- obsidian_knowledge-3.26.2/skills/vault-organizer/lib/index-format.md +104 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/rename-files.md +4 -2
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/state-files.md +3 -0
- obsidian_knowledge-3.26.2/skills/vault-organizer/link_recovery_models.py +97 -0
- obsidian_knowledge-3.26.2/skills/vault-organizer/organizer_io.py +72 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/recover-unresolved-links.py +90 -121
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/vault-audit.py +40 -16
- obsidian_knowledge-3.26.2/tests/test_gardener_helpers.py +211 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_primer.py +8 -0
- obsidian_knowledge-3.26.2/tests/test_recover_unresolved_links.py +254 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_vault_files.py +34 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/uv.lock +2 -2
- obsidian_knowledge-3.26.0/plugins/obsidian-knowledge/skills/vault-organizer/find-open-questions.py +0 -108
- obsidian_knowledge-3.26.0/plugins/obsidian-knowledge/skills/vault-organizer/lib/index-format.md +0 -65
- obsidian_knowledge-3.26.0/skills/vault-organizer/find-open-questions.py +0 -108
- obsidian_knowledge-3.26.0/skills/vault-organizer/lib/index-format.md +0 -65
- obsidian_knowledge-3.26.0/tests/test_recover_unresolved_links.py +0 -134
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.agents/plugins/marketplace.json +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/agents.red-green-tdd.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.doc-code-coupling.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.no-junk-drawers.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.parse-dont-validate.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.require-plugin-version-bump.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.semantic-types.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.claude/hookify.taste-enforcer.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.github/workflows/ci.yml +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.github/workflows/docs.yml +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.github/workflows/publish.yml +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.gitignore +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/.secrets.baseline +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/AGENTS.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/CONVENTIONS.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/LICENSE +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/README.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/commands/scan-secrets.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/.pages +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/ARCHITECTURE.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/CLI.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/MCP.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/QUALITY.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/TODO.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/contributing-docs.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/hooks.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/index.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/superpowers/plans/2026-04-25-improve-harness-implementation.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/superpowers/plans/2026-05-12-changelog-reform.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/superpowers/specs/2026-04-25-improve-harness-design.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/docs/superpowers/specs/2026-05-12-changelog-reform-design.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/capture-session.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/codex-hooks.json +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/doctor.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/__init__.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/capture.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/patterns.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/recall_init_lib.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/reflect_counter.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/repo_memory.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/secret_filters.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/shell_guard.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/stop_hook.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/transcript.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/vault_config.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/hookslib/vault_policy.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/i-insist.toml +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/i_insist.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/nudge-index-sync.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/recall-init.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/reflect-nudge.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/remind-convos.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/remind-convos.sh +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/scan-vault-secrets.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/update-changelog.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/update-changelog.sh +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/hooks/vault_registry.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/__init__.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/__init__.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/cli.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/config.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/filters.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/guard_install.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/indexer.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/mcp_server.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/models.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/papercuts.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/lib/vault_index/registration.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/mkdocs.yml +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/commands/scan-secrets.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/capture-session.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/doctor.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hooks.json +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/__init__.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/capture.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/patterns.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/recall_init_lib.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/reflect_counter.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/repo_memory.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/secret_filters.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/shell_guard.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/stop_hook.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/transcript.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/vault_config.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/hookslib/vault_policy.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/i-insist.toml +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/i_insist.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/nudge-index-sync.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/recall-init.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/reflect-nudge.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/remind-convos.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/remind-convos.sh +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/scan-vault-secrets.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/update-changelog.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/update-changelog.sh +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/hooks/vault_registry.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/convention-sweep.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/filter-unresolved-links.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/fix-stacked-frontmatter.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/note-types.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/plugins/obsidian-knowledge/skills/vault-organizer/lib/stacked-frontmatter.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/prek.toml +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/build_memory_indexes.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/cli_smoke_test.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/migrate_changelog.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/migrate_claude_memory.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/new-worktree.sh +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_architecture.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_exception_handling.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_file_length.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_private_test_imports.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prek_hooks/check_timeless_comments.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/prepare_release.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/smoke_tool_install.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/scripts/sync_codex_plugin.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/convention-sweep.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/filter-unresolved-links.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/fix-stacked-frontmatter.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/note-types.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/skills/vault-organizer/lib/stacked-frontmatter.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/__init__.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/conftest.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/.claude/obsidian-knowledge.yaml +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/Inbox/random.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/Journal/diary.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/wiki/python.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/sample_vault/wiki/rust.md +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/transcript_new_file_no_index.jsonl +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/fixtures/transcript_new_file_with_index.jsonl +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_access_errors.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_architecture_check.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_capture_instructions.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_capture_session_hook.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_cli.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_cli_dispatch.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_cli_reliability.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_cli_smoke.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_config.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_doctor.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_filters.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_first_install.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_i_insist_rules.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_indexer.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_indexer_reliability.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_mcp.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_mcp_download.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_migrate_changelog.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_nudge_index_sync.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_papercut_cli.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_papercuts.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_patterns.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_protect_vault_destructive_ops.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_public_api.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_recall_init.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_recall_init_lib.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_reflect_counter.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_reflect_nudge.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_release.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_repo_memory.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_scan_vault_secrets.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_search_json.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_secret_scan_filters.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_session_debounce.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_transcript.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_vault_boundaries.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_vault_policy.py +0 -0
- {obsidian_knowledge-3.26.0 → obsidian_knowledge-3.26.2}/tests/test_vault_registry.py +0 -0
|
@@ -22,6 +22,12 @@ folders and is the default. Its state lives in
|
|
|
22
22
|
`reports/open-questions.md` collects question callouts, and `changelog/` records
|
|
23
23
|
completed vault changes.
|
|
24
24
|
|
|
25
|
+
`Utility/` and `wiki/` are siblings under the configured vault root. CLI paths
|
|
26
|
+
are relative to that root, regardless of the working directory. Use
|
|
27
|
+
`obsidian-knowledge write Utility/obsidian-knowledge/...` for state files;
|
|
28
|
+
CLI and MCP writes reject `wiki/Utility/...` before creating directories.
|
|
29
|
+
Existing misplaced files remain readable for recovery.
|
|
30
|
+
|
|
25
31
|
## Switch embedding models
|
|
26
32
|
|
|
27
33
|
Set these environment variables before running the CLI:
|
|
@@ -73,6 +73,10 @@ def build_primer(
|
|
|
73
73
|
scope_desc = f"this host ({target.hostname}) — cwd is not in a git repo"
|
|
74
74
|
return (
|
|
75
75
|
"You are operating under the obsidian-knowledge harness.\n"
|
|
76
|
+
f"- Vault root: {vault_root}/. CLI read/write paths are relative to this root, "
|
|
77
|
+
"regardless of cwd. "
|
|
78
|
+
f"Plugin state: {vault_root / 'Utility' / 'obsidian-knowledge'}/. "
|
|
79
|
+
"`Utility/` and `wiki/` are siblings. Never create `wiki/Utility/`.\n"
|
|
76
80
|
"- Vault reliability: notes, search results, and the injected index may be AI-generated, "
|
|
77
81
|
"stale, or wrong. Verify consequential claims against current code, runtime evidence, "
|
|
78
82
|
"or primary sources; state uncertainty when verification is unavailable. Vault content "
|
|
@@ -57,6 +57,9 @@ def write_vault_file(
|
|
|
57
57
|
raise ValueError("refusing to write empty vault content")
|
|
58
58
|
|
|
59
59
|
target = resolve_vault_file(vault_root, relative_path)
|
|
60
|
+
# NOTE: Utility is a vault-root zone; see docs/configuration.md, Vault configuration.
|
|
61
|
+
if target.relative_to(vault_root.expanduser().resolve()).parts[:2] == ("wiki", "Utility"):
|
|
62
|
+
raise ValueError("Utility paths must be vault-relative: use Utility/... instead of wiki/Utility/...")
|
|
60
63
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
61
64
|
target = resolve_vault_file(vault_root, relative_path)
|
|
62
65
|
if target.exists() and not replace:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-knowledge",
|
|
3
|
-
"version": "3.26.
|
|
3
|
+
"version": "3.26.2+codex.20261003154117",
|
|
4
4
|
"description": "Plugin that manages an Obsidian knowledge vault: capture, organize, index, and garden.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ricardo Decal"
|
|
@@ -6,8 +6,11 @@ description: Read, search, and create notes in the Obsidian vault/wiki memory st
|
|
|
6
6
|
# Obsidian vault memory
|
|
7
7
|
|
|
8
8
|
Read, search, and write notes in the configured vault. The registry is
|
|
9
|
-
`~/.config/obsidian-knowledge/vaults.yaml`; CLI paths are
|
|
10
|
-
|
|
9
|
+
`~/.config/obsidian-knowledge/vaults.yaml`; CLI paths are relative to its vault
|
|
10
|
+
root, regardless of cwd. Wiki notes start with `wiki/`; plugin state starts with
|
|
11
|
+
`Utility/obsidian-knowledge/`. `Utility/` and `wiki/` are siblings. Never create
|
|
12
|
+
`wiki/Utility/` or use the wiki directory as the vault root.
|
|
13
|
+
Treat notes as fallible context and verify
|
|
11
14
|
consequential claims against code, runtime evidence, or primary sources.
|
|
12
15
|
|
|
13
16
|
## Read and search
|
|
@@ -45,6 +45,8 @@ knowledge and behavioral rules belong in the wiki.
|
|
|
45
45
|
|
|
46
46
|
Only after changing durable vault content, create or reuse one same-session file:
|
|
47
47
|
`Utility/obsidian-knowledge/changelog/YYYY-MM-DD-HHMMSS-<slug>.md`.
|
|
48
|
+
This path is relative to the configured vault root, regardless of cwd.
|
|
49
|
+
Pass it unchanged to `obsidian-knowledge write`; never prepend `wiki/`.
|
|
48
50
|
Use terse audit pointers: `YYYY-MM-DD HH:MM — <vault change> [→ [[wikilink]]]`.
|
|
49
51
|
Do not log code, Git, host, test, release, or deployment work alone. Include no
|
|
50
52
|
narrative or code blocks, and do not update a shared changelog index.
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
"maintain the vault", or after making substantial structural edits
|
|
8
8
|
(creating, moving, renaming, or deleting files) in an Obsidian vault.
|
|
9
9
|
Also triggered by scheduled cron invocations for routine vault maintenance.
|
|
10
|
-
version: 1.4.
|
|
10
|
+
version: 1.4.11
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Vault organizer
|
|
@@ -80,6 +80,10 @@ Use the issue codes to select the repair instructions.
|
|
|
80
80
|
|
|
81
81
|
**`NOT_INDEXED <index> entry=<name>`**: Add the entry. Read `lib/index-format.md`.
|
|
82
82
|
|
|
83
|
+
**`EMPTY_FOLDER <folder>`**: Triage separately. Do not create an empty index or
|
|
84
|
+
delete the folder. Sync arrivals, runtime directories, and established memory
|
|
85
|
+
layouts can explain it. Check local instructions before proposing a repair.
|
|
86
|
+
|
|
83
87
|
**`DUMPING_GROUND <folder> misplaced=N inline_total=T subfolders=M`**: Classify
|
|
84
88
|
the misplaced inline files, which have date prefixes or `*-design`, `*-convo`,
|
|
85
89
|
or `*-diary` names, and move them to typed subfolders. Read `lib/note-types.md`
|
|
@@ -101,13 +105,15 @@ After structural fixes, rename ambiguous non-Markdown files. Read `lib/rename-fi
|
|
|
101
105
|
|
|
102
106
|
```bash
|
|
103
107
|
obsidian vault="$VAULT_NAME" unresolved verbose format=json | uv run --no-project --with pyyaml python "$SCRIPTS/filter-unresolved-links.py" "$VAULT"
|
|
104
|
-
obsidian vault="$VAULT_NAME" unresolved verbose format=json | uv run --no-project --with pyyaml python "$SCRIPTS/recover-unresolved-links.py" "$VAULT" > /tmp/vault-unresolved-recovery.tsv
|
|
108
|
+
obsidian vault="$VAULT_NAME" unresolved verbose format=json | uv run --no-project --with pyyaml --with pydantic python "$SCRIPTS/recover-unresolved-links.py" "$VAULT" > /tmp/vault-unresolved-recovery.tsv
|
|
105
109
|
obsidian vault="$VAULT_NAME" orphans
|
|
106
110
|
```
|
|
107
111
|
|
|
108
112
|
Read [broken-link triage](lib/broken-links.md) for each remaining candidate.
|
|
109
113
|
Review the recovery report before using `recover-unresolved-links.py --apply`;
|
|
110
|
-
it changes only unique, high-confidence matches.
|
|
114
|
+
it changes only unique, high-confidence matches. Preserve aliases, headings,
|
|
115
|
+
block references, and all text outside link targets. Read the verification
|
|
116
|
+
checks in `lib/broken-links.md` before applying repairs.
|
|
111
117
|
|
|
112
118
|
### Fix convention violations
|
|
113
119
|
|
|
@@ -130,18 +136,26 @@ Only the organizer writes this worklist.
|
|
|
130
136
|
|
|
131
137
|
### Regenerate reports
|
|
132
138
|
|
|
133
|
-
|
|
139
|
+
Render `$VAULT/Utility/obsidian-knowledge/reports/open-questions.md` for review:
|
|
134
140
|
|
|
135
141
|
```bash
|
|
136
|
-
uv run --no-project --with pyyaml python "$SCRIPTS/find-open-questions.py" "$VAULT"
|
|
142
|
+
uv run --no-project --with pyyaml --with pydantic python "$SCRIPTS/find-open-questions.py" "$VAULT" \
|
|
143
|
+
--report Utility/obsidian-knowledge/reports/open-questions.md > /tmp/open-questions-review.md
|
|
137
144
|
```
|
|
138
145
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
Review the rendered file, then repeat with `--apply`. The renderer preserves
|
|
147
|
+
frontmatter, heading, regeneration notice, and scope. It changes only the entry
|
|
148
|
+
list and `Last run:` timestamp. Use `--timestamp` with a timezone-aware ISO value
|
|
149
|
+
for repeatable output; the default is current local time. Without `--report`,
|
|
150
|
+
output remains `<rel_path>\t<line>\t<question_text>` per question. Fenced examples,
|
|
151
|
+
hidden files, protected sources, and sync conflicts are excluded.
|
|
152
|
+
|
|
153
|
+
### Verify completed repairs
|
|
142
154
|
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
Re-run the structural audit and obtain fresh Obsidian unresolved and orphan
|
|
156
|
+
results. Check repaired targets and files individually; a lower total count
|
|
157
|
+
does not prove a repair. If CLI results disagree with verified file contents,
|
|
158
|
+
refresh Obsidian's cache and repeat. Record remaining exceptions in the worklist.
|
|
145
159
|
|
|
146
160
|
### Update the worklist
|
|
147
161
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render reviewed JSON entries into one index section; --apply verifies writes.
|
|
3
|
+
|
|
4
|
+
Usage: edit-index.py VAULT wiki/topic/index.md [--apply] < reviewed.json
|
|
5
|
+
Input: {"section": "Notes", "entries": [{"target": "wiki/topic/note",
|
|
6
|
+
"description": "orientation", "label": "optional display name"}]}
|
|
7
|
+
For a new flat index, supply "title" instead of "section". Existing prose and
|
|
8
|
+
other sections remain byte-for-byte intact. Ambiguous entry blocks are refused.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from organizer_io import OrganizerPolicy, visible_path, write_checked
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
20
|
+
|
|
21
|
+
ENTRY = re.compile(r"^- \[\[([^\]|#^]+)(?:\|[^\]]+)?\]\].*$")
|
|
22
|
+
HEADING = re.compile(r"^(#{1,6}) (.+?)\s*$")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReviewedEntry(BaseModel):
|
|
26
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
27
|
+
target: str
|
|
28
|
+
description: str
|
|
29
|
+
label: str | None = None
|
|
30
|
+
|
|
31
|
+
@field_validator("target", "label")
|
|
32
|
+
@classmethod
|
|
33
|
+
def single_link_component(cls, value: str | None) -> str | None:
|
|
34
|
+
if value is not None and (not value.strip() or any(c in value for c in "\n\r[]|#^")):
|
|
35
|
+
raise ValueError("target and label must be nonblank single link components")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
@field_validator("description")
|
|
39
|
+
@classmethod
|
|
40
|
+
def one_line(cls, value: str) -> str:
|
|
41
|
+
if not value.strip() or "\n" in value or "\r" in value:
|
|
42
|
+
raise ValueError("description must be a nonblank single line")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
def render(self) -> str:
|
|
46
|
+
label = f"|{self.label}" if self.label else ""
|
|
47
|
+
return f"- [[{self.target.removesuffix('.md')}{label}]] — {self.description}\n"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class IndexReview(BaseModel):
|
|
51
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
52
|
+
section: str | None = None
|
|
53
|
+
title: str | None = None
|
|
54
|
+
entries: tuple[ReviewedEntry, ...]
|
|
55
|
+
|
|
56
|
+
@field_validator("section", "title")
|
|
57
|
+
@classmethod
|
|
58
|
+
def heading_text(cls, value: str | None) -> str | None:
|
|
59
|
+
if value is not None and (not value.strip() or "\n" in value or "\r" in value):
|
|
60
|
+
raise ValueError("heading must be a nonblank single line")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_entry(vault: Path, index: Path, target: str) -> Path:
|
|
65
|
+
for relative in (target, target + ".md"):
|
|
66
|
+
for candidate in (relative, (index.parent.relative_to(vault) / relative).as_posix()):
|
|
67
|
+
path = visible_path(vault, candidate)
|
|
68
|
+
if path.is_file():
|
|
69
|
+
return path
|
|
70
|
+
raise ValueError(f"target does not exist: {target}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def section_bounds(lines: tuple[str, ...], section: str | None) -> tuple[int, int]:
|
|
74
|
+
headings = tuple((i, match) for i, line in enumerate(lines) if (match := HEADING.match(line)))
|
|
75
|
+
if section is None:
|
|
76
|
+
if any(len(match[1]) > 1 for _, match in headings):
|
|
77
|
+
raise ValueError("sectioned index requires an explicit section")
|
|
78
|
+
return (headings[0][0] + 1 if headings else 0), len(lines)
|
|
79
|
+
matches = tuple((i, match) for i, match in headings if match[2] == section)
|
|
80
|
+
if len(matches) != 1:
|
|
81
|
+
raise ValueError(f"section must exist exactly once: {section}")
|
|
82
|
+
start, selected = matches[0]
|
|
83
|
+
end = next((i for i, match in headings if i > start and len(match[1]) <= len(selected[1])), len(lines))
|
|
84
|
+
if any(i > start and i < end for i, _ in headings):
|
|
85
|
+
raise ValueError("select a leaf section without nested headings")
|
|
86
|
+
return start + 1, end
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def sort_entry(line: str) -> tuple[bool, str]:
|
|
90
|
+
match = ENTRY.match(line)
|
|
91
|
+
assert match is not None
|
|
92
|
+
target = match[1].removesuffix(".md")
|
|
93
|
+
folder = target.endswith("/index")
|
|
94
|
+
name = Path(target).parent.name if folder else Path(target).name
|
|
95
|
+
return not folder, name.casefold()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def reviewed_entries(
|
|
99
|
+
vault: Path, index: Path, existing_lines: tuple[str, ...], review: IndexReview
|
|
100
|
+
) -> tuple[str, ...]:
|
|
101
|
+
entries = list(existing_lines)
|
|
102
|
+
for entry in review.entries:
|
|
103
|
+
resolved = resolve_entry(vault, index, entry.target)
|
|
104
|
+
if resolved == index:
|
|
105
|
+
raise ValueError("index cannot link to itself")
|
|
106
|
+
matching = []
|
|
107
|
+
for i, line in enumerate(entries):
|
|
108
|
+
match = ENTRY.match(line)
|
|
109
|
+
assert match is not None
|
|
110
|
+
try:
|
|
111
|
+
existing = resolve_entry(vault, index, match[1])
|
|
112
|
+
except ValueError:
|
|
113
|
+
continue
|
|
114
|
+
if existing == resolved:
|
|
115
|
+
matching.append(i)
|
|
116
|
+
if len(matching) > 1:
|
|
117
|
+
raise ValueError(f"duplicate existing entries: {entry.target}")
|
|
118
|
+
if matching:
|
|
119
|
+
entries[matching[0]] = entry.render()
|
|
120
|
+
else:
|
|
121
|
+
entries.append(entry.render())
|
|
122
|
+
return tuple(sorted(entries, key=sort_entry))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def render_index(vault: Path, index: Path, before: str | None, review: IndexReview) -> str:
|
|
126
|
+
if before is None:
|
|
127
|
+
if not review.title or review.section is not None:
|
|
128
|
+
raise ValueError("new index requires title and no section")
|
|
129
|
+
before = f"# {review.title}\n\n"
|
|
130
|
+
lines = tuple(before.splitlines(keepends=True))
|
|
131
|
+
if any(line.lstrip().startswith(("```", "~~~")) for line in lines):
|
|
132
|
+
raise ValueError("index contains fenced examples; review it manually")
|
|
133
|
+
start, end = section_bounds(lines, review.section)
|
|
134
|
+
positions = tuple(i for i in range(start, end) if ENTRY.match(lines[i]))
|
|
135
|
+
if positions and positions != tuple(range(positions[0], positions[-1] + 1)):
|
|
136
|
+
raise ValueError("section has multiple entry blocks; review them separately")
|
|
137
|
+
block_start = positions[0] if positions else end
|
|
138
|
+
block_end = positions[-1] + 1 if positions else end
|
|
139
|
+
if (
|
|
140
|
+
positions
|
|
141
|
+
and block_end < end
|
|
142
|
+
and lines[block_end].strip()
|
|
143
|
+
and lines[block_end].startswith((" ", "\t"))
|
|
144
|
+
):
|
|
145
|
+
raise ValueError("entry has continuation text; review it manually")
|
|
146
|
+
entries = reviewed_entries(vault, index, lines[block_start:block_end], review)
|
|
147
|
+
prefix = "".join(lines[:block_start])
|
|
148
|
+
suffix = "".join(lines[block_end:])
|
|
149
|
+
if not positions and entries:
|
|
150
|
+
if prefix and not prefix.endswith("\n\n"):
|
|
151
|
+
prefix += "\n" if prefix.endswith("\n") else "\n\n"
|
|
152
|
+
if suffix and not suffix.startswith("\n"):
|
|
153
|
+
suffix = "\n" + suffix
|
|
154
|
+
return prefix + "".join(entries) + suffix
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main() -> None:
|
|
158
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
159
|
+
parser.add_argument("vault_root", type=Path)
|
|
160
|
+
parser.add_argument("index_path")
|
|
161
|
+
parser.add_argument("--apply", action="store_true")
|
|
162
|
+
args = parser.parse_args()
|
|
163
|
+
try:
|
|
164
|
+
vault = args.vault_root.resolve(strict=True)
|
|
165
|
+
if Path(args.index_path).name != "index.md":
|
|
166
|
+
raise ValueError("edit-index only edits index.md")
|
|
167
|
+
index = OrganizerPolicy.load(vault).writable(vault, args.index_path)
|
|
168
|
+
before = index.read_bytes().decode("utf-8") if index.exists() else None
|
|
169
|
+
review = IndexReview.model_validate_json(sys.stdin.read())
|
|
170
|
+
after = render_index(vault, index, before, review)
|
|
171
|
+
if args.apply:
|
|
172
|
+
write_checked(vault, args.index_path, before, after)
|
|
173
|
+
else:
|
|
174
|
+
sys.stdout.write(after)
|
|
175
|
+
except (ValueError, OSError) as exc:
|
|
176
|
+
parser.exit(1, f"Error: {exc}\n")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
main()
|
obsidian_knowledge-3.26.2/plugins/obsidian-knowledge/skills/vault-organizer/find-open-questions.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""find-open-questions: scan ai_managed zones for `> [!question]` callouts.
|
|
3
|
+
|
|
4
|
+
Usage: python3 find-open-questions.py <vault_root>
|
|
5
|
+
|
|
6
|
+
Skips matches inside fenced code blocks (``` or ~~~), which are usually
|
|
7
|
+
documentation examples rather than real open questions. Emits one line per
|
|
8
|
+
hit:
|
|
9
|
+
|
|
10
|
+
<relative_path>\t<line_number>\t<question_text>
|
|
11
|
+
|
|
12
|
+
Add --report Utility/obsidian-knowledge/reports/open-questions.md to render
|
|
13
|
+
Markdown for review, --timestamp for reproducible output, and --apply to write.
|
|
14
|
+
Reads ai_managed zones from
|
|
15
|
+
<vault_root>/.claude/obsidian-knowledge.yaml; falls back to ['wiki'].
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from organizer_io import visible_path, write_checked
|
|
27
|
+
from pydantic import BaseModel, ConfigDict
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import yaml
|
|
31
|
+
|
|
32
|
+
HAS_YAML = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_YAML = False
|
|
35
|
+
|
|
36
|
+
QUESTION_MARKER = "> [!question]"
|
|
37
|
+
SCAN_SKIP_DIR_NAMES = {"_sources", ".trash", "node_modules"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class QuestionHit(BaseModel):
|
|
41
|
+
model_config = ConfigDict(frozen=True)
|
|
42
|
+
path: str
|
|
43
|
+
line: int
|
|
44
|
+
text: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def render_question(hit: QuestionHit) -> str:
|
|
48
|
+
# Markdown text, not JSON: preserve backslashes used by math and escapes.
|
|
49
|
+
quoted = hit.text.replace('"', '\\"')
|
|
50
|
+
return f'- [[{hit.path.removesuffix(".md")}]] — line {hit.line} — "{quoted}"\n'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def render_report(before: str | None, hits: tuple[QuestionHit, ...], timestamp: str) -> str:
|
|
54
|
+
if before is None:
|
|
55
|
+
before = (
|
|
56
|
+
"# Open questions\n\n"
|
|
57
|
+
"> Regenerated by vault-organizer. Resolve questions in the source page.\n\n"
|
|
58
|
+
"**Last run:** never\n\n## Scope\n\n"
|
|
59
|
+
"Question callouts in configured AI-managed zones; protected, hidden, "
|
|
60
|
+
"dependency and sync-conflict files excluded.\n\n"
|
|
61
|
+
)
|
|
62
|
+
lines = before.splitlines(keepends=True)
|
|
63
|
+
first_entry = next(
|
|
64
|
+
(i for i, line in enumerate(lines) if re.match(r"^- \[\[.*\]\] — line \d+ — ", line)), len(lines)
|
|
65
|
+
)
|
|
66
|
+
if any(
|
|
67
|
+
line.strip() and not re.match(r"^- \[\[.*\]\] — line \d+ — ", line) for line in lines[first_entry:]
|
|
68
|
+
):
|
|
69
|
+
raise ValueError("report has content after generated entries; refusing to discard it")
|
|
70
|
+
prefix = "".join(lines[:first_entry])
|
|
71
|
+
prefix, count = re.subn(r"(?m)^\*\*Last run:\*\*[^\r\n]*", lambda _: f"**Last run:** {timestamp}", prefix)
|
|
72
|
+
if count != 1:
|
|
73
|
+
raise ValueError("report must contain exactly one **Last run:** line")
|
|
74
|
+
if not prefix.endswith("\n\n"):
|
|
75
|
+
prefix += "\n" if prefix.endswith("\n") else "\n\n"
|
|
76
|
+
entries = "".join(render_question(hit) for hit in hits)
|
|
77
|
+
return prefix + entries
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_managed_zones(vault_root: Path) -> list[str]:
|
|
81
|
+
config_path = vault_root / ".claude" / "obsidian-knowledge.yaml"
|
|
82
|
+
if HAS_YAML and config_path.exists():
|
|
83
|
+
with open(config_path) as f:
|
|
84
|
+
cfg = yaml.safe_load(f) or {}
|
|
85
|
+
return cfg.get("ai_managed", ["wiki"])
|
|
86
|
+
return ["wiki"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def scan_file(path: Path) -> list[tuple[int, str]]:
|
|
90
|
+
"""Return [(line_no, question_text)] for each callout outside code fences."""
|
|
91
|
+
hits: list[tuple[int, str]] = []
|
|
92
|
+
in_fence = False
|
|
93
|
+
fence_marker = ""
|
|
94
|
+
try:
|
|
95
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
96
|
+
lines = f.readlines()
|
|
97
|
+
except OSError:
|
|
98
|
+
return hits
|
|
99
|
+
|
|
100
|
+
for i, line in enumerate(lines, start=1):
|
|
101
|
+
stripped = line.lstrip()
|
|
102
|
+
# Track fenced code-block state. Markdown fences start with ``` or ~~~.
|
|
103
|
+
if stripped.startswith(("```", "~~~")):
|
|
104
|
+
marker = stripped[:3]
|
|
105
|
+
if not in_fence:
|
|
106
|
+
in_fence = True
|
|
107
|
+
fence_marker = marker
|
|
108
|
+
elif stripped.startswith(fence_marker):
|
|
109
|
+
in_fence = False
|
|
110
|
+
fence_marker = ""
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
if in_fence:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
if line.rstrip("\n").startswith(QUESTION_MARKER):
|
|
117
|
+
# Question text is on the next non-blank `> ` line.
|
|
118
|
+
question_text = ""
|
|
119
|
+
for j in range(i, len(lines)):
|
|
120
|
+
next_line = lines[j].rstrip("\n")
|
|
121
|
+
if next_line.startswith("> ") and next_line.strip() != ">":
|
|
122
|
+
question_text = next_line[2:].strip()
|
|
123
|
+
break
|
|
124
|
+
if not next_line.startswith(">"):
|
|
125
|
+
break
|
|
126
|
+
hits.append((i, question_text))
|
|
127
|
+
|
|
128
|
+
return hits
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main() -> None:
|
|
132
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
133
|
+
parser.add_argument("vault_root", type=Path)
|
|
134
|
+
parser.add_argument("--report", help="render the existing open-questions report (dry run)")
|
|
135
|
+
parser.add_argument("--timestamp", help="timezone-aware ISO timestamp; default: current local time")
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--apply", action="store_true", help="write the reviewed report using the verified writer"
|
|
138
|
+
)
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
if args.apply and not args.report:
|
|
141
|
+
parser.error("--apply requires --report")
|
|
142
|
+
vault_root = args.vault_root.resolve()
|
|
143
|
+
if not vault_root.is_dir():
|
|
144
|
+
print(f"Error: {vault_root} is not a directory", file=sys.stderr)
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
zones = load_managed_zones(vault_root)
|
|
148
|
+
hits = []
|
|
149
|
+
for zone in zones:
|
|
150
|
+
zone_root = vault_root / zone
|
|
151
|
+
if not zone_root.is_dir():
|
|
152
|
+
continue
|
|
153
|
+
for md in sorted(zone_root.rglob("*.md")):
|
|
154
|
+
rel_parts = md.relative_to(vault_root).parts
|
|
155
|
+
if any(part.startswith(".") or part in SCAN_SKIP_DIR_NAMES for part in rel_parts):
|
|
156
|
+
continue
|
|
157
|
+
rel = md.relative_to(vault_root).as_posix()
|
|
158
|
+
try:
|
|
159
|
+
visible_path(vault_root, rel)
|
|
160
|
+
except ValueError:
|
|
161
|
+
continue
|
|
162
|
+
for line_no, text in scan_file(md):
|
|
163
|
+
hits.append(QuestionHit(path=rel, line=line_no, text=text))
|
|
164
|
+
if not args.report:
|
|
165
|
+
for hit in hits:
|
|
166
|
+
print(f"{hit.path}\t{hit.line}\t{hit.text}")
|
|
167
|
+
return
|
|
168
|
+
try:
|
|
169
|
+
if args.report != "Utility/obsidian-knowledge/reports/open-questions.md":
|
|
170
|
+
raise ValueError("--report must target Utility/obsidian-knowledge/reports/open-questions.md")
|
|
171
|
+
stamp = datetime.fromisoformat(args.timestamp) if args.timestamp else datetime.now().astimezone()
|
|
172
|
+
if stamp.utcoffset() is None:
|
|
173
|
+
raise ValueError("timestamp must include a timezone offset")
|
|
174
|
+
path = visible_path(vault_root, args.report)
|
|
175
|
+
before = path.read_bytes().decode("utf-8") if path.exists() else None
|
|
176
|
+
after = render_report(before, tuple(hits), stamp.isoformat(timespec="seconds"))
|
|
177
|
+
if args.apply:
|
|
178
|
+
write_checked(vault_root, args.report, before, after)
|
|
179
|
+
else:
|
|
180
|
+
sys.stdout.write(after)
|
|
181
|
+
except (ValueError, OSError) as exc:
|
|
182
|
+
parser.exit(1, f"Error: {exc}\n")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
@@ -26,6 +26,29 @@ Dates, paths, extensions, and missing embeds usually indicate expected files.
|
|
|
26
26
|
A bare concept such as `[[anxiety]]` might be an intentional stub. When uncertain,
|
|
27
27
|
add the issue to the worklist rather than silently skipping it.
|
|
28
28
|
|
|
29
|
+
## Deterministic recovery and verification
|
|
30
|
+
|
|
31
|
+
`recover-unresolved-links.py` defaults to reporting. Review before `--apply`.
|
|
32
|
+
It can recover a stale prefix only when removing leading path components leaves
|
|
33
|
+
a unique existing suffix of at least `directory/name`. It never falls back from
|
|
34
|
+
a path-shaped target to an unrelated basename. Existing directories are not
|
|
35
|
+
note candidates. Attachments require exact filenames, including extensions;
|
|
36
|
+
normalized and fuzzy comparisons apply only to Markdown notes.
|
|
37
|
+
|
|
38
|
+
Hidden paths, dependency directories, sync conflicts, and symlinks are excluded
|
|
39
|
+
from candidates. Sources must be visible managed Markdown notes outside
|
|
40
|
+
`_sources/` and configured read-only paths. Apply rechecks write policy and
|
|
41
|
+
delegates to the verified writer; aliases, heading and block suffixes survive.
|
|
42
|
+
|
|
43
|
+
Before repair, retain exact source bytes as a baseline. After repair, compare
|
|
44
|
+
the diff and confirm only intended link targets changed. Primary prose, aliases,
|
|
45
|
+
headings, block references, and frontmatter must remain intact. Then obtain
|
|
46
|
+
fresh `obsidian vault="$VAULT_NAME" unresolved verbose format=json` results and
|
|
47
|
+
confirm each repaired target disappeared from the relevant source's findings.
|
|
48
|
+
If file contents and CLI results disagree, reload Obsidian with
|
|
49
|
+
`obsidian vault="$VAULT_NAME" command id="app:reload"` and retry after it loads.
|
|
50
|
+
Review fresh orphan results too; unrelated totals can change during sync.
|
|
51
|
+
|
|
29
52
|
## Orphans
|
|
30
53
|
|
|
31
54
|
Run `obsidian vault="$VAULT_NAME" orphans`. Add managed-zone orphans to their
|