jfox-cli 0.5.0__tar.gz → 0.6.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.
Files changed (144) hide show
  1. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/PKG-INFO +1 -1
  2. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/__init__.py +1 -1
  3. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/cli.py +37 -106
  4. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/config.py +23 -2
  5. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/daemon/process.py +38 -5
  6. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/daemon/server.py +19 -1
  7. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/template_cli.py +5 -23
  8. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/pyproject.toml +1 -1
  9. jfox_cli-0.6.0/tests/unit/test_use_kb_env_var.py +94 -0
  10. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/uv.lock +1 -1
  11. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/.claude/settings.local.json +0 -0
  12. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/.claude/skills/release/SKILL.md +0 -0
  13. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/.claude/skills/release/release_helper.py +0 -0
  14. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/.githooks/pre-push +0 -0
  15. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/.github/workflows/integration-test.yml +0 -0
  16. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/.github/workflows/publish.yml +0 -0
  17. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/.gitignore +0 -0
  18. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/.python-version +0 -0
  19. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/AGENTS.md +0 -0
  20. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/CHANGELOG.md +0 -0
  21. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/CLAUDE.md +0 -0
  22. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/DEVELOPMENT_PLAN.md +0 -0
  23. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/README.md +0 -0
  24. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/SESSION.md +0 -0
  25. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/SESSION_SUMMARY.md +0 -0
  26. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/installation.md +0 -0
  27. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-11-bulk-import-bm25-fix.md +0 -0
  28. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-11-edit-command.md +0 -0
  29. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-11-unify-format-option.md +0 -0
  30. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md +0 -0
  31. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-edit-content-file.md +0 -0
  32. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md +0 -0
  33. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md +0 -0
  34. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-fix-jfox-health-skill-kb-param.md +0 -0
  35. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-index-kb-param.md +0 -0
  36. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-lazy-import-perf.md +0 -0
  37. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-skill-redesign.md +0 -0
  38. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-13-add-content-file.md +0 -0
  39. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-13-bulk-import-vectorstore-init.md +0 -0
  40. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-13-ingest-log-command.md +0 -0
  41. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-13-ingest-skill-sync.md +0 -0
  42. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-13-suppress-third-party-logging.md +0 -0
  43. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-13-sync-skills-with-cli.md +0 -0
  44. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-14-sync-docs-daemon-show.md +0 -0
  45. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-15-gpu-embedding.md +0 -0
  46. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-15-make-daemon-deps-required.md +0 -0
  47. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-15-session-summary-confirmation.md +0 -0
  48. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-16-fix-windows-daemon-console-window.md +0 -0
  49. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-18-daemon-timeout-and-index-rebuild.md +0 -0
  50. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-22-release-skill.md +0 -0
  51. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-26-fix-daemon-deprecation-warnings.md +0 -0
  52. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-27-intranet-model-download.md +0 -0
  53. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-28-tag-filtering.md +0 -0
  54. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-03-bugfixes-design.md +0 -0
  55. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md +0 -0
  56. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md +0 -0
  57. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-14-show-command-design.md +0 -0
  58. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-15-gpu-embedding-design.md +0 -0
  59. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-15-session-summary-confirmation-design.md +0 -0
  60. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-21-release-skill-design.md +0 -0
  61. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-26-fix-daemon-deprecation-warnings-design.md +0 -0
  62. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-27-intranet-model-download-design.md +0 -0
  63. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-28-tag-filtering-design.md +0 -0
  64. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/docs/troubleshooting.md +0 -0
  65. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jessica-jones-static-cable.md +0 -0
  66. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/__main__.py +0 -0
  67. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/bm25_index.py +0 -0
  68. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/daemon/__init__.py +0 -0
  69. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/daemon/__main__.py +0 -0
  70. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/daemon/client.py +0 -0
  71. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/embedding_backend.py +0 -0
  72. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/formatters.py +0 -0
  73. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/git_extractor.py +0 -0
  74. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/global_config.py +0 -0
  75. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/graph.py +0 -0
  76. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/indexer.py +0 -0
  77. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/kb_manager.py +0 -0
  78. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/model_downloader.py +0 -0
  79. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/models.py +0 -0
  80. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/note.py +0 -0
  81. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/performance.py +0 -0
  82. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/search_engine.py +0 -0
  83. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/template.py +0 -0
  84. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/jfox/vector_store.py +0 -0
  85. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/pytest.ini +0 -0
  86. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/run_full_test.ps1 +0 -0
  87. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/scripts/download-model-intranet.sh +0 -0
  88. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/README.md +0 -0
  89. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-common/SKILL.md +0 -0
  90. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-ingest/SKILL.md +0 -0
  91. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-organize/SKILL.md +0 -0
  92. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-search/SKILL.md +0 -0
  93. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-session-summary/SKILL.md +0 -0
  94. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-common/SKILL.md +0 -0
  95. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-ingest/SKILL.md +0 -0
  96. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-organize/SKILL.md +0 -0
  97. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-search/SKILL.md +0 -0
  98. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-session-summary/SKILL.md +0 -0
  99. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/COVERAGE_PLAN.md +0 -0
  100. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/MIGRATION.md +0 -0
  101. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/TESTS.md +0 -0
  102. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/conftest.py +0 -0
  103. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/integration/__init__.py +0 -0
  104. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/integration/test_backlinks.py +0 -0
  105. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/integration/test_model_download.py +0 -0
  106. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/integration/test_tag_filter_cli.py +0 -0
  107. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/performance/__init__.py +0 -0
  108. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/performance/test_performance.py +0 -0
  109. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_advanced_features.py +0 -0
  110. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_cli_format.py +0 -0
  111. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_config_set_unit.py +0 -0
  112. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_config_unit.py +0 -0
  113. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_core_workflow.py +0 -0
  114. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_embedding_device.py +0 -0
  115. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_hybrid_search.py +0 -0
  116. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_integration.py +0 -0
  117. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_kb_current.py +0 -0
  118. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/test_suggest_links.py +0 -0
  119. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/__init__.py +0 -0
  120. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_bm25_batch.py +0 -0
  121. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_daemon_process.py +0 -0
  122. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_edit.py +0 -0
  123. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_format_unify.py +0 -0
  124. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_formatters.py +0 -0
  125. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_git_extractor.py +0 -0
  126. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_global_config.py +0 -0
  127. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_index_kb_param.py +0 -0
  128. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_indexer_clear_before_rebuild.py +0 -0
  129. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_indexer_verify.py +0 -0
  130. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_kb_manager.py +0 -0
  131. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_lazy_import.py +0 -0
  132. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_logging_config.py +0 -0
  133. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_model_downloader.py +0 -0
  134. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_release_helper.py +0 -0
  135. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_show.py +0 -0
  136. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_tag_filter.py +0 -0
  137. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_template.py +0 -0
  138. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_template_cli.py +0 -0
  139. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/unit/test_vector_store_clear.py +0 -0
  140. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/utils/__init__.py +0 -0
  141. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/utils/assertions.py +0 -0
  142. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/utils/jfox_cli.py +0 -0
  143. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/utils/note_generator.py +0 -0
  144. {jfox_cli-0.5.0 → jfox_cli-0.6.0}/tests/utils/temp_kb.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jfox-cli
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: JFox - Zettelkasten 知识管理 CLI 工具
5
5
  Project-URL: Homepage, https://github.com/zhuxixi/jfox
6
6
  Project-URL: Repository, https://github.com/zhuxixi/jfox
@@ -1,5 +1,5 @@
1
1
  """JFox - Zettelkasten 知识管理工具"""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
4
4
  __author__ = "User"
5
5
  __email__ = "user@example.com"
@@ -424,13 +424,9 @@ def add(
424
424
  if not content:
425
425
  raise ValueError("请提供笔记内容(位置参数或 --content-file)")
426
426
 
427
- # 如果指定了知识库,临时切换
428
- if kb:
429
- from .config import use_kb
427
+ from .config import use_kb
430
428
 
431
- with use_kb(kb):
432
- _add_note_impl(content, title, note_type, tags, source, output_format, template)
433
- else:
429
+ with use_kb(kb):
434
430
  _add_note_impl(content, title, note_type, tags, source, output_format, template)
435
431
 
436
432
  except typer.Exit:
@@ -561,13 +557,9 @@ def search(
561
557
  if json_output:
562
558
  output_format = "json"
563
559
 
564
- # 如果指定了知识库,临时切换
565
- if kb:
566
- from .config import use_kb
560
+ from .config import use_kb
567
561
 
568
- with use_kb(kb):
569
- _search_impl(query, top, note_type, tags, search_mode, output_format)
570
- else:
562
+ with use_kb(kb):
571
563
  _search_impl(query, top, note_type, tags, search_mode, output_format)
572
564
 
573
565
  except Exception as e:
@@ -746,13 +738,9 @@ def status(
746
738
  ):
747
739
  """查看知识库状态"""
748
740
  try:
749
- # 如果指定了知识库,临时切换
750
- if kb:
751
- from .config import use_kb
741
+ from .config import use_kb
752
742
 
753
- with use_kb(kb):
754
- _status_impl(output_format, json_output)
755
- else:
743
+ with use_kb(kb):
756
744
  _status_impl(output_format, json_output)
757
745
 
758
746
  except Exception as e:
@@ -847,13 +835,9 @@ def list(
847
835
  if json_output:
848
836
  output_format = "json"
849
837
 
850
- # 如果指定了知识库,临时切换
851
- if kb:
852
- from .config import use_kb
838
+ from .config import use_kb
853
839
 
854
- with use_kb(kb):
855
- _list_impl(note_type, tags, limit, output_format)
856
- else:
840
+ with use_kb(kb):
857
841
  _list_impl(note_type, tags, limit, output_format)
858
842
 
859
843
  except Exception as e:
@@ -897,12 +881,9 @@ def show(
897
881
  支持通过笔记 ID 或标题定位。
898
882
  """
899
883
  try:
900
- if kb:
901
- from .config import use_kb
884
+ from .config import use_kb
902
885
 
903
- with use_kb(kb):
904
- _show_impl(note_ref)
905
- else:
886
+ with use_kb(kb):
906
887
  _show_impl(note_ref)
907
888
 
908
889
  except Exception as e:
@@ -1046,13 +1027,9 @@ def refs(
1046
1027
  if json_output:
1047
1028
  output_format = "json"
1048
1029
 
1049
- # 如果指定了知识库,临时切换
1050
- if kb:
1051
- from .config import use_kb
1030
+ from .config import use_kb
1052
1031
 
1053
- with use_kb(kb):
1054
- _refs_impl(note_id, search, output_format, json_output)
1055
- else:
1032
+ with use_kb(kb):
1056
1033
  _refs_impl(note_id, search, output_format, json_output)
1057
1034
 
1058
1035
  except Exception as e:
@@ -1125,13 +1102,9 @@ def delete(
1125
1102
  if json_output:
1126
1103
  output_format = "json"
1127
1104
 
1128
- # 如果指定了知识库,临时切换
1129
- if kb:
1130
- from .config import use_kb
1105
+ from .config import use_kb
1131
1106
 
1132
- with use_kb(kb):
1133
- _delete_impl(note_id, force, output_format)
1134
- else:
1107
+ with use_kb(kb):
1135
1108
  _delete_impl(note_id, force, output_format)
1136
1109
 
1137
1110
  except typer.Exit:
@@ -1331,14 +1304,9 @@ def edit(
1331
1304
  if json_output:
1332
1305
  output_format = "json"
1333
1306
 
1334
- if kb:
1335
- from .config import use_kb
1307
+ from .config import use_kb
1336
1308
 
1337
- with use_kb(kb):
1338
- _edit_impl(
1339
- note_id, content, content_file, title, tags, note_type, source, output_format
1340
- )
1341
- else:
1309
+ with use_kb(kb):
1342
1310
  _edit_impl(
1343
1311
  note_id, content, content_file, title, tags, note_type, source, output_format
1344
1312
  )
@@ -1443,13 +1411,9 @@ def query(
1443
1411
  ):
1444
1412
  """语义搜索 + 知识图谱联合查询"""
1445
1413
  try:
1446
- # 如果指定了知识库,临时切换
1447
- if kb:
1448
- from .config import use_kb
1414
+ from .config import use_kb
1449
1415
 
1450
- with use_kb(kb):
1451
- _query_impl(query_str, top, graph_depth, json_output)
1452
- else:
1416
+ with use_kb(kb):
1453
1417
  _query_impl(query_str, top, graph_depth, json_output)
1454
1418
 
1455
1419
  except Exception as e:
@@ -1579,13 +1543,9 @@ def graph(
1579
1543
  if json_output:
1580
1544
  output_format = "json"
1581
1545
 
1582
- # 如果指定了知识库,临时切换
1583
- if kb:
1584
- from .config import use_kb
1546
+ from .config import use_kb
1585
1547
 
1586
- with use_kb(kb):
1587
- _graph_impl(note_id, depth, stats, orphans, output_format, json_output)
1588
- else:
1548
+ with use_kb(kb):
1589
1549
  _graph_impl(note_id, depth, stats, orphans, output_format, json_output)
1590
1550
 
1591
1551
  except Exception as e:
@@ -1654,13 +1614,9 @@ def daily(
1654
1614
  if json_output:
1655
1615
  output_format = "json"
1656
1616
 
1657
- # 如果指定了知识库,临时切换
1658
- if kb:
1659
- from .config import use_kb
1617
+ from .config import use_kb
1660
1618
 
1661
- with use_kb(kb):
1662
- _daily_impl(date, output_format, json_output)
1663
- else:
1619
+ with use_kb(kb):
1664
1620
  _daily_impl(date, output_format, json_output)
1665
1621
 
1666
1622
  except Exception as e:
@@ -1766,13 +1722,9 @@ def suggest_links(
1766
1722
  output_format = "json"
1767
1723
 
1768
1724
  try:
1769
- # 如果指定了知识库,临时切换
1770
- if kb:
1771
- from .config import use_kb
1725
+ from .config import use_kb
1772
1726
 
1773
- with use_kb(kb):
1774
- _suggest_links_impl(content, top_k, threshold, output_format, json_output)
1775
- else:
1727
+ with use_kb(kb):
1776
1728
  _suggest_links_impl(content, top_k, threshold, output_format, json_output)
1777
1729
 
1778
1730
  except Exception as e:
@@ -1799,13 +1751,9 @@ def inbox(
1799
1751
  if json_output:
1800
1752
  output_format = "json"
1801
1753
 
1802
- # 如果指定了知识库,临时切换
1803
- if kb:
1804
- from .config import use_kb
1754
+ from .config import use_kb
1805
1755
 
1806
- with use_kb(kb):
1807
- _inbox_impl(limit, output_format, json_output)
1808
- else:
1756
+ with use_kb(kb):
1809
1757
  _inbox_impl(limit, output_format, json_output)
1810
1758
 
1811
1759
  except Exception as e:
@@ -1985,13 +1933,9 @@ def index(
1985
1933
  if json_output:
1986
1934
  output_format = "json"
1987
1935
 
1988
- # 如果指定了知识库,临时切换
1989
- if kb:
1990
- from .config import use_kb
1936
+ from .config import use_kb
1991
1937
 
1992
- with use_kb(kb):
1993
- _index_impl(action, output_format)
1994
- else:
1938
+ with use_kb(kb):
1995
1939
  _index_impl(action, output_format)
1996
1940
 
1997
1941
  except typer.Exit:
@@ -2453,15 +2397,9 @@ def ingest_log(
2453
2397
  if json_output:
2454
2398
  output_format = "json"
2455
2399
 
2456
- # 如果指定了知识库,临时切换
2457
- if kb:
2458
- from .config import use_kb
2400
+ from .config import use_kb
2459
2401
 
2460
- with use_kb(kb):
2461
- _ingest_log_impl(
2462
- repo_path, limit, note_type, batch_size, output_format, json_output
2463
- )
2464
- else:
2402
+ with use_kb(kb):
2465
2403
  _ingest_log_impl(repo_path, limit, note_type, batch_size, output_format, json_output)
2466
2404
 
2467
2405
  except ValueError as e:
@@ -2512,20 +2450,10 @@ def bulk_import(
2512
2450
 
2513
2451
  console.print(f"[yellow]Importing {len(notes_data)} notes...[/yellow]")
2514
2452
 
2453
+ from .config import use_kb
2515
2454
  from .performance import bulk_import_notes
2516
2455
 
2517
- # 如果指定了知识库,临时切换
2518
- if kb:
2519
- from .config import use_kb
2520
-
2521
- with use_kb(kb):
2522
- result = bulk_import_notes(
2523
- notes_data=notes_data,
2524
- note_type=note_type,
2525
- batch_size=batch_size,
2526
- show_progress=not json_output,
2527
- )
2528
- else:
2456
+ with use_kb(kb):
2529
2457
  result = bulk_import_notes(
2530
2458
  notes_data=notes_data,
2531
2459
  note_type=note_type,
@@ -2659,8 +2587,11 @@ def daemon(
2659
2587
  console.print("[dim]Daemon 未运行[/dim]")
2660
2588
  return
2661
2589
  console.print("[yellow]正在停止 daemon...[/yellow]")
2662
- stop_daemon()
2663
- console.print("[green]✓ Daemon 已停止[/green]")
2590
+ if stop_daemon():
2591
+ console.print("[green]✓ Daemon 已停止[/green]")
2592
+ else:
2593
+ console.print("[red]✗ Daemon 停止失败,请手动终止进程[/red]")
2594
+ raise typer.Exit(1)
2664
2595
 
2665
2596
  elif action == "status":
2666
2597
  info = get_daemon_status()
@@ -1,11 +1,15 @@
1
1
  """配置管理"""
2
2
 
3
+ import os
3
4
  from contextlib import contextmanager
4
5
  from dataclasses import dataclass, field
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
8
9
  import yaml
10
+ from rich.console import Console
11
+
12
+ _console = Console(stderr=True)
9
13
 
10
14
 
11
15
  def get_default_kb_path() -> Path:
@@ -148,15 +152,26 @@ def use_kb(kb_name: Optional[str] = None):
148
152
  # 在这个上下文中,操作都在 work 知识库上
149
153
  note = create_note(...)
150
154
 
155
+ 优先级: --kb > JFOX_KB > 全局配置 default
156
+ 当 kb_name 为 None 时,会先检查 JFOX_KB 环境变量,
157
+ 若未设置则使用全局配置中的默认知识库。
158
+
151
159
  Args:
152
160
  kb_name: 知识库名称,None 表示使用当前默认知识库
153
161
 
154
162
  Raises:
155
163
  ValueError: 知识库不存在
156
164
  """
165
+ resolved_from_env = False
166
+
157
167
  if not kb_name:
158
- yield
159
- return
168
+ env_kb = os.environ.get("JFOX_KB", "").strip()
169
+ if env_kb:
170
+ kb_name = env_kb
171
+ resolved_from_env = True
172
+ else:
173
+ yield
174
+ return
160
175
 
161
176
  from .kb_manager import get_kb_manager
162
177
 
@@ -182,6 +197,12 @@ def use_kb(kb_name: Optional[str] = None):
182
197
  # 重置索引和搜索引擎(使用新的知识库路径)
183
198
  _reset_singletons()
184
199
 
200
+ if resolved_from_env:
201
+ _console.print(
202
+ f"Using knowledge base '{kb_name}' (from JFOX_KB environment variable)",
203
+ style="dim",
204
+ )
205
+
185
206
  yield
186
207
  finally:
187
208
  # 恢复原始配置
@@ -88,6 +88,19 @@ def _http_health_check(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> Op
88
88
  return None
89
89
 
90
90
 
91
+ def _http_shutdown(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> bool:
92
+ """请求 daemon 通过 /shutdown endpoint 自行停止"""
93
+ try:
94
+ import urllib.request
95
+
96
+ req = urllib.request.Request(f"http://{host}:{port}/shutdown", method="POST", data=b"")
97
+ with urllib.request.urlopen(req, timeout=3) as resp:
98
+ result = json.loads(resp.read().decode("utf-8"))
99
+ return result.get("status") == "shutting_down"
100
+ except (OSError, ValueError):
101
+ return False
102
+
103
+
91
104
  def is_daemon_running() -> bool:
92
105
  """检查 daemon 是否在运行(以 HTTP 健康检查为准)"""
93
106
  # 先尝试 PID 文件记录的地址
@@ -276,11 +289,32 @@ def stop_daemon() -> bool:
276
289
  port = data.get("port", DEFAULT_PORT)
277
290
 
278
291
  # 先检查是否真的在跑
279
- if _http_health_check(host, port) is None:
292
+ health = _http_health_check(host, port)
293
+ if health is None:
280
294
  _remove_pid_file()
281
295
  return True
282
296
 
283
- # 尝试停止
297
+ # 1. 优先通过 HTTP /shutdown 让 daemon 自行退出
298
+ logger.info("正在通过 /shutdown 请求 daemon 停止...")
299
+ shutdown_ok = _http_shutdown(host, port)
300
+
301
+ if shutdown_ok:
302
+ # 等待 daemon 自行退出(最多 3 秒)
303
+ for _ in range(6):
304
+ if _http_health_check(host, port) is None:
305
+ _remove_pid_file()
306
+ logger.info("Daemon 已通过 /shutdown 停止")
307
+ return True
308
+ time.sleep(0.5)
309
+
310
+ # 2. /shutdown 失败或超时,从 /health 获取真实 PID 后尝试 taskkill/kill
311
+ if pid == 0:
312
+ health = _http_health_check(host, port)
313
+ if health:
314
+ pid = health.get("pid", 0)
315
+ if not isinstance(pid, int):
316
+ pid = 0
317
+
284
318
  if pid > 0:
285
319
  try:
286
320
  if sys.platform == "win32":
@@ -294,7 +328,7 @@ def stop_daemon() -> bool:
294
328
  except (OSError, subprocess.SubprocessError) as e:
295
329
  logger.warning(f"停止 daemon 失败: {e}")
296
330
 
297
- # 等待进程退出
331
+ # 等待进程退出(最多 5 秒)
298
332
  for _ in range(10):
299
333
  if _http_health_check(host, port) is None:
300
334
  _remove_pid_file()
@@ -302,9 +336,8 @@ def stop_daemon() -> bool:
302
336
  return True
303
337
  time.sleep(0.5)
304
338
 
305
- # 超时未退出
339
+ # 超时未退出 — 不删 PID 文件,保留追踪信息
306
340
  logger.warning(f"Daemon 停止超时 (PID: {pid})")
307
- _remove_pid_file()
308
341
  return False
309
342
 
310
343
 
@@ -19,6 +19,9 @@ logger = logging.getLogger(__name__)
19
19
  # 全局 embedding 后端(模型加载后常驻内存)
20
20
  _backend = None
21
21
 
22
+ # uvicorn Server 实例(用于 graceful shutdown)
23
+ _server = None
24
+
22
25
 
23
26
  def _load_model():
24
27
  """启动时加载模型(标记为 daemon 进程,防止自引用)"""
@@ -76,6 +79,10 @@ class EncodeSingleRequest(BaseModel):
76
79
  text: str
77
80
 
78
81
 
82
+ class ShutdownResponse(BaseModel):
83
+ status: str
84
+
85
+
79
86
  class EncodeSingleResponse(BaseModel):
80
87
  embedding: List[float]
81
88
  dimension: int
@@ -98,6 +105,14 @@ def health():
98
105
  )
99
106
 
100
107
 
108
+ @app.post("/shutdown", response_model=ShutdownResponse)
109
+ def shutdown():
110
+ """请求 daemon 自行停止"""
111
+ if _server:
112
+ _server.should_exit = True
113
+ return ShutdownResponse(status="shutting_down")
114
+
115
+
101
116
  @app.post("/encode", response_model=EncodeResponse)
102
117
  def encode(req: EncodeRequest):
103
118
  """批量文本编码"""
@@ -133,7 +148,10 @@ def main():
133
148
 
134
149
  import uvicorn
135
150
 
136
- uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
151
+ global _server
152
+ config = uvicorn.Config(app, host=args.host, port=args.port, log_level="warning")
153
+ _server = uvicorn.Server(config)
154
+ _server.run()
137
155
 
138
156
 
139
157
  if __name__ == "__main__":
@@ -42,11 +42,7 @@ def list_templates(
42
42
  if json_output:
43
43
  output_format = "json"
44
44
 
45
- if kb:
46
- with use_kb(kb):
47
- manager = get_template_manager()
48
- templates = manager.list_templates()
49
- else:
45
+ with use_kb(kb):
50
46
  manager = get_template_manager()
51
47
  templates = manager.list_templates()
52
48
 
@@ -120,11 +116,7 @@ def show_template(
120
116
  try:
121
117
  from .config import use_kb
122
118
 
123
- if kb:
124
- with use_kb(kb):
125
- manager = get_template_manager()
126
- template = manager.get_template(name)
127
- else:
119
+ with use_kb(kb):
128
120
  manager = get_template_manager()
129
121
  template = manager.get_template(name)
130
122
 
@@ -183,10 +175,7 @@ def create_template(
183
175
  try:
184
176
  from .config import use_kb
185
177
 
186
- if kb:
187
- with use_kb(kb):
188
- manager = get_template_manager()
189
- else:
178
+ with use_kb(kb):
190
179
  manager = get_template_manager()
191
180
 
192
181
  # Check if template exists
@@ -246,11 +235,7 @@ def edit_template(
246
235
  try:
247
236
  from .config import use_kb
248
237
 
249
- if kb:
250
- with use_kb(kb):
251
- manager = get_template_manager()
252
- template = manager.get_template(name)
253
- else:
238
+ with use_kb(kb):
254
239
  manager = get_template_manager()
255
240
  template = manager.get_template(name)
256
241
 
@@ -294,10 +279,7 @@ def remove_template(
294
279
  try:
295
280
  from .config import use_kb
296
281
 
297
- if kb:
298
- with use_kb(kb):
299
- manager = get_template_manager()
300
- else:
282
+ with use_kb(kb):
301
283
  manager = get_template_manager()
302
284
 
303
285
  template = manager.get_template(name)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "jfox-cli"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "JFox - Zettelkasten 知识管理 CLI 工具"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,94 @@
1
+ """
2
+ 测试类型: 单元测试
3
+ 目标模块: jfox.config.use_kb
4
+ 预估耗时: < 1秒
5
+ 依赖要求: 无外部依赖,使用 mock
6
+ """
7
+
8
+ import os
9
+
10
+ import pytest
11
+
12
+ pytestmark = [pytest.mark.unit, pytest.mark.fast]
13
+ from unittest.mock import Mock, patch
14
+
15
+ from jfox.config import config, use_kb
16
+
17
+
18
+ class TestUseKbEnvVar:
19
+ """测试 use_kb() 对 JFOX_KB 环境变量的支持"""
20
+
21
+ def test_env_var_used_when_kb_arg_none(self):
22
+ """当 kb_name 为 None 且 JFOX_KB 设置时,应切换到该知识库"""
23
+ with patch.dict(os.environ, {"JFOX_KB": "work"}):
24
+ with patch("jfox.config._reset_singletons"):
25
+ with patch("jfox.kb_manager.get_kb_manager") as mock_get_manager:
26
+ mock_manager = Mock()
27
+ mock_manager.config_manager.kb_exists.return_value = True
28
+ mock_manager.config_manager.get_kb_path.return_value = (
29
+ config.base_dir.parent / "work"
30
+ )
31
+ mock_get_manager.return_value = mock_manager
32
+
33
+ with use_kb(None) as _:
34
+ # 验证确实切换了知识库
35
+ mock_manager.config_manager.get_kb_path.assert_called_once_with("work")
36
+
37
+ def test_cli_arg_overrides_env_var(self):
38
+ """当同时传入 kb_name 和设置 JFOX_KB 时,应优先使用传入的 kb_name"""
39
+ with patch.dict(os.environ, {"JFOX_KB": "work"}):
40
+ with patch("jfox.config._reset_singletons"):
41
+ with patch("jfox.kb_manager.get_kb_manager") as mock_get_manager:
42
+ mock_manager = Mock()
43
+ mock_manager.config_manager.kb_exists.return_value = True
44
+ mock_manager.config_manager.get_kb_path.return_value = (
45
+ config.base_dir.parent / "personal"
46
+ )
47
+ mock_get_manager.return_value = mock_manager
48
+
49
+ with use_kb("personal") as _:
50
+ # 验证使用的是传入的 "personal",而不是环境变量的 "work"
51
+ mock_manager.config_manager.get_kb_path.assert_called_once_with("personal")
52
+
53
+ def test_invalid_jfox_kb_raises_valueerror(self):
54
+ """当 JFOX_KB 指向不存在的知识库时,应抛出 ValueError"""
55
+ with patch.dict(os.environ, {"JFOX_KB": "nonexistent"}):
56
+ with patch("jfox.config._reset_singletons"):
57
+ with patch("jfox.kb_manager.get_kb_manager") as mock_get_manager:
58
+ mock_manager = Mock()
59
+ mock_manager.config_manager.kb_exists.return_value = False
60
+ mock_get_manager.return_value = mock_manager
61
+
62
+ with pytest.raises(ValueError, match="Knowledge base 'nonexistent' not found"):
63
+ with use_kb(None):
64
+ pass
65
+
66
+ def test_no_env_var_falls_back_to_global_default(self):
67
+ """当没有设置 JFOX_KB 且 kb_name 为 None 时,应直接使用当前默认知识库"""
68
+ with patch.dict(os.environ, {}, clear=False):
69
+ os.environ.pop("JFOX_KB", None)
70
+ with patch("jfox.kb_manager.get_kb_manager") as mock_get_manager:
71
+ # 不应调用 get_kb_manager,因为没有需要切换的知识库
72
+ original_base_dir = config.base_dir
73
+
74
+ with use_kb(None) as _:
75
+ # 验证没有尝试获取 kb_manager
76
+ mock_get_manager.assert_not_called()
77
+ # 验证配置没有被修改
78
+ assert config.base_dir == original_base_dir
79
+
80
+ def test_jfox_kb_with_whitespace_is_stripped(self):
81
+ """JFOX_KB 值包含首尾空格时应被去除"""
82
+ with patch.dict(os.environ, {"JFOX_KB": " work "}):
83
+ with patch("jfox.config._reset_singletons"):
84
+ with patch("jfox.kb_manager.get_kb_manager") as mock_get_manager:
85
+ mock_manager = Mock()
86
+ mock_manager.config_manager.kb_exists.return_value = True
87
+ mock_manager.config_manager.get_kb_path.return_value = (
88
+ config.base_dir.parent / "work"
89
+ )
90
+ mock_get_manager.return_value = mock_manager
91
+
92
+ with use_kb(None) as _:
93
+ # 验证去除了空格后使用的是 "work"
94
+ mock_manager.config_manager.get_kb_path.assert_called_once_with("work")
@@ -867,7 +867,7 @@ wheels = [
867
867
 
868
868
  [[package]]
869
869
  name = "jfox-cli"
870
- version = "0.5.0"
870
+ version = "0.6.0"
871
871
  source = { editable = "." }
872
872
  dependencies = [
873
873
  { name = "chromadb" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes