jfox-cli 0.4.3__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.4.3 → jfox_cli-0.6.0}/.github/workflows/integration-test.yml +1 -1
  2. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/.gitignore +10 -0
  3. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/CHANGELOG.md +18 -0
  4. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/PKG-INFO +1 -1
  5. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-13-add-content-file.md +301 -0
  6. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-13-bulk-import-vectorstore-init.md +270 -0
  7. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-13-ingest-log-command.md +809 -0
  8. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-13-ingest-skill-sync.md +338 -0
  9. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-13-suppress-third-party-logging.md +115 -0
  10. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-13-sync-skills-with-cli.md +340 -0
  11. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-15-gpu-embedding.md +905 -0
  12. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-15-make-daemon-deps-required.md +358 -0
  13. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-16-fix-windows-daemon-console-window.md +206 -0
  14. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-18-daemon-timeout-and-index-rebuild.md +744 -0
  15. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-22-release-skill.md +746 -0
  16. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-26-fix-daemon-deprecation-warnings.md +185 -0
  17. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-27-intranet-model-download.md +828 -0
  18. jfox_cli-0.6.0/docs/superpowers/plans/2026-04-28-tag-filtering.md +1087 -0
  19. jfox_cli-0.6.0/docs/superpowers/specs/2026-04-15-gpu-embedding-design.md +190 -0
  20. jfox_cli-0.6.0/docs/superpowers/specs/2026-04-21-release-skill-design.md +175 -0
  21. jfox_cli-0.6.0/docs/superpowers/specs/2026-04-26-fix-daemon-deprecation-warnings-design.md +56 -0
  22. jfox_cli-0.6.0/docs/superpowers/specs/2026-04-27-intranet-model-download-design.md +284 -0
  23. jfox_cli-0.6.0/docs/superpowers/specs/2026-04-28-tag-filtering-design.md +146 -0
  24. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/__init__.py +1 -1
  25. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/cli.py +56 -112
  26. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/config.py +23 -2
  27. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/daemon/process.py +38 -5
  28. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/daemon/server.py +19 -1
  29. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/note.py +18 -3
  30. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/search_engine.py +41 -10
  31. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/template_cli.py +5 -23
  32. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/vector_store.py +16 -1
  33. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/pyproject.toml +1 -1
  34. jfox_cli-0.6.0/tests/integration/test_tag_filter_cli.py +81 -0
  35. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/performance/test_performance.py +1 -3
  36. jfox_cli-0.6.0/tests/unit/test_tag_filter.py +125 -0
  37. jfox_cli-0.6.0/tests/unit/test_use_kb_env_var.py +94 -0
  38. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/utils/jfox_cli.py +21 -2
  39. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/utils/temp_kb.py +3 -3
  40. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/uv.lock +1 -1
  41. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/.claude/settings.local.json +0 -0
  42. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/.claude/skills/release/SKILL.md +0 -0
  43. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/.claude/skills/release/release_helper.py +0 -0
  44. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/.githooks/pre-push +0 -0
  45. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/.github/workflows/publish.yml +0 -0
  46. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/.python-version +0 -0
  47. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/AGENTS.md +0 -0
  48. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/CLAUDE.md +0 -0
  49. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/DEVELOPMENT_PLAN.md +0 -0
  50. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/README.md +0 -0
  51. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/SESSION.md +0 -0
  52. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/SESSION_SUMMARY.md +0 -0
  53. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/installation.md +0 -0
  54. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-11-bulk-import-bm25-fix.md +0 -0
  55. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-11-edit-command.md +0 -0
  56. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-11-unify-format-option.md +0 -0
  57. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md +0 -0
  58. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-edit-content-file.md +0 -0
  59. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md +0 -0
  60. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md +0 -0
  61. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-fix-jfox-health-skill-kb-param.md +0 -0
  62. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-index-kb-param.md +0 -0
  63. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-lazy-import-perf.md +0 -0
  64. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-12-skill-redesign.md +0 -0
  65. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-14-sync-docs-daemon-show.md +0 -0
  66. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/plans/2026-04-15-session-summary-confirmation.md +0 -0
  67. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-03-bugfixes-design.md +0 -0
  68. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md +0 -0
  69. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md +0 -0
  70. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-14-show-command-design.md +0 -0
  71. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/superpowers/specs/2026-04-15-session-summary-confirmation-design.md +0 -0
  72. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/docs/troubleshooting.md +0 -0
  73. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jessica-jones-static-cable.md +0 -0
  74. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/__main__.py +0 -0
  75. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/bm25_index.py +0 -0
  76. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/daemon/__init__.py +0 -0
  77. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/daemon/__main__.py +0 -0
  78. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/daemon/client.py +0 -0
  79. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/embedding_backend.py +0 -0
  80. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/formatters.py +0 -0
  81. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/git_extractor.py +0 -0
  82. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/global_config.py +0 -0
  83. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/graph.py +0 -0
  84. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/indexer.py +0 -0
  85. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/kb_manager.py +0 -0
  86. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/model_downloader.py +0 -0
  87. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/models.py +0 -0
  88. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/performance.py +0 -0
  89. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/jfox/template.py +0 -0
  90. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/pytest.ini +0 -0
  91. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/run_full_test.ps1 +0 -0
  92. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/scripts/download-model-intranet.sh +0 -0
  93. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/README.md +0 -0
  94. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-common/SKILL.md +0 -0
  95. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-ingest/SKILL.md +0 -0
  96. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-organize/SKILL.md +0 -0
  97. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-search/SKILL.md +0 -0
  98. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/claude-code/jfox-session-summary/SKILL.md +0 -0
  99. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-common/SKILL.md +0 -0
  100. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-ingest/SKILL.md +0 -0
  101. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-organize/SKILL.md +0 -0
  102. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-search/SKILL.md +0 -0
  103. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/skills-recommend/kimi-cli/jfox-session-summary/SKILL.md +0 -0
  104. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/COVERAGE_PLAN.md +0 -0
  105. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/MIGRATION.md +0 -0
  106. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/TESTS.md +0 -0
  107. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/conftest.py +0 -0
  108. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/integration/__init__.py +0 -0
  109. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/integration/test_backlinks.py +0 -0
  110. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/integration/test_model_download.py +0 -0
  111. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/performance/__init__.py +0 -0
  112. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_advanced_features.py +0 -0
  113. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_cli_format.py +0 -0
  114. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_config_set_unit.py +0 -0
  115. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_config_unit.py +0 -0
  116. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_core_workflow.py +0 -0
  117. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_embedding_device.py +0 -0
  118. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_hybrid_search.py +0 -0
  119. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_integration.py +0 -0
  120. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_kb_current.py +0 -0
  121. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/test_suggest_links.py +0 -0
  122. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/__init__.py +0 -0
  123. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_bm25_batch.py +0 -0
  124. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_daemon_process.py +0 -0
  125. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_edit.py +0 -0
  126. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_format_unify.py +0 -0
  127. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_formatters.py +0 -0
  128. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_git_extractor.py +0 -0
  129. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_global_config.py +0 -0
  130. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_index_kb_param.py +0 -0
  131. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_indexer_clear_before_rebuild.py +0 -0
  132. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_indexer_verify.py +0 -0
  133. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_kb_manager.py +0 -0
  134. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_lazy_import.py +0 -0
  135. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_logging_config.py +0 -0
  136. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_model_downloader.py +0 -0
  137. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_release_helper.py +0 -0
  138. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_show.py +0 -0
  139. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_template.py +0 -0
  140. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_template_cli.py +0 -0
  141. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/unit/test_vector_store_clear.py +0 -0
  142. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/utils/__init__.py +0 -0
  143. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/utils/assertions.py +0 -0
  144. {jfox_cli-0.4.3 → jfox_cli-0.6.0}/tests/utils/note_generator.py +0 -0
@@ -245,7 +245,7 @@ jobs:
245
245
  run: |
246
246
  echo "lint: ${{ needs.lint.result }}"
247
247
  echo "test-fast: ${{ needs.test-fast.result }}"
248
- if [[ "${{ needs.lint.result }}" == "success" && "${{ needs.test-fast.result }}" == "success" ]]; then
248
+ if [[ "${{ needs.lint.result }}" == "success" && ("${{ needs.test-fast.result }}" == "success" || "${{ needs.test-fast.result }}" == "skipped") ]]; then
249
249
  echo "Quality gate passed!"
250
250
  exit 0
251
251
  else
@@ -37,3 +37,13 @@ Thumbs.db
37
37
  chroma_db/
38
38
  *.sqlite3
39
39
  cache/
40
+
41
+ # Claude Code runtime files
42
+ .claude/scheduled_tasks.json
43
+ .claude/scheduled_tasks.lock
44
+ .claude/worktrees/
45
+
46
+ # Temporary analysis files
47
+ analysis_report.md
48
+ kimi-export-*.md
49
+ review_body*.md
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to jfox-cli will be documented in this file.
4
4
 
5
+ ## [0.5.0] - 2026-04-29
6
+
7
+ ### Features
8
+ - 支持通过标签召回笔记 (#170) (#177)
9
+ - **cli**: show tags column in list table output
10
+ - **note**: add tags parameter to list_notes() and search_notes()
11
+ - **vector_store**: add tags parameter to search() for ChromaDB filtering
12
+
13
+ ### Fixes
14
+ - **test**: resolve CI test-full failures (#175)
15
+
16
+ ### Changes
17
+ - add superpowers plans and specs docs, update .gitignore (#178)
18
+ - add tag filtering implementation plan for #170
19
+ - add tag filtering design spec for #170
20
+
21
+ [0.5.0]: https://github.com/zhuxixi/jfox/compare/v0.4.3...v0.5.0
22
+
5
23
  ## [0.4.3] - 2026-04-28
6
24
 
7
25
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jfox-cli
3
- Version: 0.4.3
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
@@ -0,0 +1,301 @@
1
+ # add --content-file Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** `jfox add` 支持 `--content-file` 选项,从文件或 stdin 读取笔记内容。
6
+
7
+ **Architecture:** 在 `add` 命令中将 `content` 从必需位置参数改为可选参数,新增 `--content-file` 选项。文件读取逻辑复用 `edit` 命令已有的模式(`cli.py` L975-986),两者互斥校验。stdin 支持通过 `--content-file -` 实现。`_add_note_impl` 签名不变。
8
+
9
+ **Tech Stack:** Python, Typer, pathlib
10
+
11
+ ---
12
+
13
+ ### Task 1: 文件读取逻辑提取为公共函数
14
+
15
+ **Files:**
16
+ - Modify: `jfox/cli.py` (在 `_edit_impl` 之前添加公共函数)
17
+ - Modify: `jfox/cli.py:970-986` (`_edit_impl` 调用改为使用公共函数)
18
+
19
+ **为什么先做:** `add` 和 `edit` 都需要读取 `--content-file`,提取公共函数避免重复代码,也方便单独测试。
20
+
21
+ - [ ] **Step 1: 在 `_edit_impl` 之前添加 `_read_content_file()` 函数**
22
+
23
+ 在 `jfox/cli.py` 约第 958 行(`_edit_impl` 定义之前)插入:
24
+
25
+ ```python
26
+ def _read_content_file(content_file: str) -> str:
27
+ """从文件或 stdin 读取内容(--content-file 共用逻辑)"""
28
+ if content_file == "-":
29
+ import sys
30
+
31
+ return sys.stdin.read()
32
+
33
+ p = Path(content_file)
34
+ if not p.exists():
35
+ raise ValueError(f"文件不存在: {content_file}")
36
+ if not p.is_file():
37
+ raise ValueError(f"路径不是文件: {content_file}")
38
+ try:
39
+ return p.read_text(encoding="utf-8")
40
+ except PermissionError:
41
+ raise ValueError(f"无权限读取文件: {content_file}")
42
+ except UnicodeDecodeError:
43
+ raise ValueError(f"文件编码错误(需要 UTF-8): {content_file}")
44
+ ```
45
+
46
+ - [ ] **Step 2: 重构 `_edit_impl` 使用公共函数**
47
+
48
+ 将 `jfox/cli.py` L974-986(`_edit_impl` 中的文件读取代码):
49
+
50
+ ```python
51
+ # 从文件读取内容
52
+ if content_file is not None:
53
+ p = Path(content_file)
54
+ if not p.exists():
55
+ raise ValueError(f"文件不存在: {content_file}")
56
+ if not p.is_file():
57
+ raise ValueError(f"路径不是文件: {content_file}")
58
+ try:
59
+ content = p.read_text(encoding="utf-8")
60
+ except PermissionError:
61
+ raise ValueError(f"无权限读取文件: {content_file}")
62
+ except UnicodeDecodeError:
63
+ raise ValueError(f"文件编码错误(需要 UTF-8): {content_file}")
64
+ ```
65
+
66
+ 替换为:
67
+
68
+ ```python
69
+ # 从文件读取内容
70
+ if content_file is not None:
71
+ content = _read_content_file(content_file)
72
+ ```
73
+
74
+ - [ ] **Step 3: 运行快速回归测试,确认 edit 功能不变**
75
+
76
+ Run: `uv run pytest tests/test_cli_format.py::TestAddAndDeleteFormat -v`
77
+ Expected: 全部 PASS
78
+
79
+ - [ ] **Step 4: Commit**
80
+
81
+ ```bash
82
+ git add jfox/cli.py
83
+ git commit -m "refactor: extract _read_content_file() for shared --content-file logic"
84
+ ```
85
+
86
+ ---
87
+
88
+ ### Task 2: add 命令支持 --content-file 选项
89
+
90
+ **Files:**
91
+ - Modify: `jfox/cli.py:366-383` (`add` 函数签名和调用)
92
+
93
+ - [ ] **Step 1: 修改 `add()` 函数签名**
94
+
95
+ 将 `jfox/cli.py` L367-368:
96
+
97
+ ```python
98
+ @app.command()
99
+ def add(
100
+ content: str = typer.Argument(..., help="笔记内容(支持 [[笔记标题]] 格式链接)"),
101
+ ```
102
+
103
+ 改为:
104
+
105
+ ```python
106
+ @app.command()
107
+ def add(
108
+ content: Optional[str] = typer.Argument(None, help="笔记内容(支持 [[笔记标题]] 格式链接)"),
109
+ ```
110
+
111
+ - [ ] **Step 2: 在 `--template` 选项之后添加 `--content-file` 参数**
112
+
113
+ 在 `jfox/cli.py` L375-377(`template` 参数定义之后)插入:
114
+
115
+ ```python
116
+ content_file: Optional[str] = typer.Option(
117
+ None, "--content-file", help="从文件读取内容(用 - 表示 stdin)"
118
+ ),
119
+ ```
120
+
121
+ - [ ] **Step 3: 在 `add()` 函数体中添加互斥校验和文件读取逻辑**
122
+
123
+ 将 `jfox/cli.py` L384-397:
124
+
125
+ ```python
126
+ """添加新笔记(内容中可用 [[笔记标题]] 引用其他笔记)"""
127
+ try:
128
+ # 向后兼容:--json 快捷方式
129
+ if json_output:
130
+ output_format = "json"
131
+
132
+ # 如果指定了知识库,临时切换
133
+ if kb:
134
+ from .config import use_kb
135
+
136
+ with use_kb(kb):
137
+ _add_note_impl(content, title, note_type, tags, source, output_format, template)
138
+ else:
139
+ _add_note_impl(content, title, note_type, tags, source, output_format, template)
140
+ ```
141
+
142
+ 改为:
143
+
144
+ ```python
145
+ """添加新笔记(内容中可用 [[笔记标题]] 引用其他笔记)"""
146
+ try:
147
+ # 向后兼容:--json 快捷方式
148
+ if json_output:
149
+ output_format = "json"
150
+
151
+ # content 和 --content-file 互斥
152
+ if content is not None and content_file is not None:
153
+ raise ValueError("不能同时指定内容参数和 --content-file,请选择其一")
154
+
155
+ # 从文件读取内容
156
+ if content_file is not None:
157
+ content = _read_content_file(content_file)
158
+
159
+ # 至少提供一种内容来源
160
+ if not content:
161
+ raise ValueError("请提供笔记内容(位置参数或 --content-file)")
162
+
163
+ # 如果指定了知识库,临时切换
164
+ if kb:
165
+ from .config import use_kb
166
+
167
+ with use_kb(kb):
168
+ _add_note_impl(content, title, note_type, tags, source, output_format, template)
169
+ else:
170
+ _add_note_impl(content, title, note_type, tags, source, output_format, template)
171
+ ```
172
+
173
+ - [ ] **Step 4: Commit**
174
+
175
+ ```bash
176
+ git add jfox/cli.py
177
+ git commit -m "feat: add --content-file option to jfox add command"
178
+ ```
179
+
180
+ ---
181
+
182
+ ### Task 3: 测试 --content-file 功能
183
+
184
+ **Files:**
185
+ - Modify: `tests/test_cli_format.py` (在 `TestAddAndDeleteFormat` 类中添加测试)
186
+
187
+ - [ ] **Step 1: 在 `test_add_format_json` 之前添加 --content-file 测试**
188
+
189
+ 在 `tests/test_cli_format.py` 的 `TestAddAndDeleteFormat` 类中,`test_add_format_json` (L327) 之前插入:
190
+
191
+ ```python
192
+ def test_add_content_file(self, cli, tmp_path):
193
+ """测试 add 命令 --content-file 从文件读取内容"""
194
+ content_file = tmp_path / "note.md"
195
+ content_file.write_text("从文件读取的笔记内容", encoding="utf-8")
196
+
197
+ result = cli.run("add", "--content-file", str(content_file), "--title", "FileContent")
198
+
199
+ assert result.success
200
+ data = result.data
201
+ assert data["success"] is True
202
+ assert data["note"]["title"] == "FileContent"
203
+
204
+ def test_add_content_file_mutual_exclusive(self, cli, tmp_path):
205
+ """测试 add 命令 content 参数和 --content-file 不能同时指定"""
206
+ content_file = tmp_path / "note.md"
207
+ content_file.write_text("file content", encoding="utf-8")
208
+
209
+ result = cli.run("add", "直接内容", "--content-file", str(content_file))
210
+
211
+ assert not result.success
212
+ assert "不能同时指定" in result.stderr or "不能同时指定" in result.stdout
213
+
214
+ def test_add_content_file_not_found(self, cli):
215
+ """测试 add 命令 --content-file 文件不存在时报错"""
216
+ result = cli.run("add", "--content-file", "/nonexistent/file.md")
217
+
218
+ assert not result.success
219
+ assert "文件不存在" in result.stderr or "文件不存在" in result.stdout
220
+
221
+ def test_add_content_file_empty(self, cli, tmp_path):
222
+ """测试 add 命令不提供内容时报错"""
223
+ result = cli.run("add")
224
+
225
+ assert not result.success
226
+
227
+ def test_add_content_file_with_wiki_links(self, cli, tmp_path):
228
+ """测试 add 命令 --content-file 内容中的 [[wiki链接]] 正常解析"""
229
+ # 先创建一个目标笔记
230
+ cli.add("目标笔记内容", title="目标笔记")
231
+
232
+ content_file = tmp_path / "linked.md"
233
+ content_file.write_text("引用 [[目标笔记]] 的内容", encoding="utf-8")
234
+
235
+ result = cli.run("add", "--content-file", str(content_file), "--title", "带链接的笔记")
236
+
237
+ assert result.success
238
+ data = result.data
239
+ assert data["note"]["links"]
240
+ ```
241
+
242
+ - [ ] **Step 2: 运行测试确认全部通过**
243
+
244
+ Run: `uv run pytest tests/test_cli_format.py::TestAddAndDeleteFormat -v`
245
+ Expected: 全部 PASS(包括新增的 5 个测试)
246
+
247
+ - [ ] **Step 3: Commit**
248
+
249
+ ```bash
250
+ git add tests/test_cli_format.py
251
+ git commit -m "test: add --content-file tests for jfox add command"
252
+ ```
253
+
254
+ ---
255
+
256
+ ### Task 4: 验证 edit --content-file 回归
257
+
258
+ **Files:** 无修改
259
+
260
+ - [ ] **Step 1: 确认 edit 的 --content-file 仍然正常工作**
261
+
262
+ 检查现有 edit 测试是否覆盖 --content-file:
263
+
264
+ ```bash
265
+ uv run pytest tests/test_cli_format.py -v -k "edit"
266
+ ```
267
+
268
+ 如果 edit 的 --content-file 没有现有测试,手动验证:
269
+
270
+ ```bash
271
+ uv run jfox add "原始内容" --title "编辑测试" --json
272
+ # 记录返回的 note_id
273
+ echo "新内容" > /tmp/test_edit.md
274
+ uv run jfox edit <note_id> --content-file /tmp/test_edit.md --json
275
+ ```
276
+
277
+ Expected: edit 成功,内容更新为 "新内容"
278
+
279
+ - [ ] **Step 2: 确认 add 的原有行为不受影响**
280
+
281
+ ```bash
282
+ uv run jfox add "直接输入的内容" --title "位置参数测试" --json
283
+ ```
284
+
285
+ Expected: add 成功,内容为 "直接输入的内容"
286
+
287
+ ---
288
+
289
+ ## Self-Review Checklist
290
+
291
+ | 检查项 | 状态 |
292
+ |--------|------|
293
+ | `_read_content_file` 覆盖文件不存在/非文件/权限/编码错误 | Done (Task 1) |
294
+ | stdin 支持 (`--content-file -`) | Done (Task 1, `content_file == "-"` 分支) |
295
+ | `content` 和 `--content-file` 互斥校验 | Done (Task 2) |
296
+ | 都不指定时报错提示 | Done (Task 2) |
297
+ | `--content-file` 与 `--template` 交互正确(内容作为 `{{content}}` 变量) | Done (Task 2, `_add_note_impl` 未修改,`content` 变量直接传入) |
298
+ | `-f` 短选项不被占用 | Done (Task 2, `--content-file` 无短选项) |
299
+ | `wiki_links` 提取仍正常工作 | Done (Task 3 测试覆盖) |
300
+ | `edit` 命令回归安全 | Done (Task 1 提取公共函数,Task 4 验证) |
301
+ | `_add_note_impl` 签名未修改 | Done (Task 2 只在 `add()` 中读取文件后传入 `content: str`) |
@@ -0,0 +1,270 @@
1
+ # Bulk-Import VectorStore 初始化修复 实现计划
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** 修复 `bulk_import_notes()` 中 VectorStore.collection 未初始化导致批量索引静默失败的问题,同时让 `index rebuild` 命令同时重建 BM25 索引。
6
+
7
+ **Architecture:** 两处独立修复:(1) `performance.py` 在批量索引前确保 VectorStore 已初始化;(2) `cli.py` 的 `index rebuild` 命令增加 BM25 重建步骤。两处修复互不依赖。
8
+
9
+ **Tech Stack:** Python 3.10+, pytest, unittest.mock
10
+
11
+ ---
12
+
13
+ ## Task 1: 修复 bulk_import_notes 中 VectorStore 未初始化的 bug
14
+
15
+ **Files:**
16
+ - Modify: `jfox/performance.py:266`
17
+ - Test: `tests/unit/test_bm25_batch.py`
18
+
19
+ - [ ] **Step 1: 写一个能复现 bug 的失败测试**
20
+
21
+ 在 `tests/unit/test_bm25_batch.py` 的 `TestBulkImportBM25Integration` 类中,添加一个测试:模拟 `VectorStore` 的 `collection` 为 `None`(即真实场景),验证 `bulk_import_notes` 不会抛异常。
22
+
23
+ ```python
24
+ @patch("jfox.bm25_index.get_bm25_index")
25
+ @patch("jfox.embedding_backend.get_backend")
26
+ @patch("jfox.note.create_note")
27
+ def test_bulk_import_with_uninitialized_vectorstore(
28
+ self, mock_create_note, mock_get_backend, mock_get_bm25, tmp_path
29
+ ):
30
+ """bulk_import_notes 应在 collection.add() 前自动初始化 VectorStore"""
31
+ import numpy as np
32
+
33
+ from jfox.models import Note, NoteType
34
+ from jfox.performance import bulk_import_notes
35
+
36
+ # 准备 mock note
37
+ mock_note = MagicMock(spec=Note)
38
+ mock_note.id = "20260413120000"
39
+ mock_note.title = "测试初始化"
40
+ mock_note.content = "内容"
41
+ mock_note.type = NoteType.PERMANENT
42
+ mock_note.tags = []
43
+ mock_note.filepath = tmp_path / "notes" / "permanent" / "test.md"
44
+ mock_note.to_markdown.return_value = "# 测试初始化\n内容"
45
+ mock_create_note.return_value = mock_note
46
+
47
+ # mock embedding backend
48
+ mock_backend = MagicMock()
49
+ mock_backend.model = MagicMock()
50
+ mock_backend.encode.return_value = np.array([[0.1] * 384])
51
+ mock_get_backend.return_value = mock_backend
52
+
53
+ # mock BM25
54
+ mock_bm25 = MagicMock()
55
+ mock_bm25.add_documents_batch.return_value = True
56
+ mock_get_bm25.return_value = mock_bm25
57
+
58
+ # 不 mock get_vector_store,让真实的 VectorStore 被创建
59
+ # 但 patch 掉 VectorStore.init 来验证它被调用了
60
+ with patch("jfox.performance.get_vector_store") as mock_get_vs:
61
+ mock_vs = MagicMock()
62
+ # 关键:collection 初始为 None,模拟真实场景
63
+ mock_vs.collection = None
64
+
65
+ # init() 会设置 collection
66
+ def fake_init():
67
+ mock_vs.collection = MagicMock()
68
+
69
+ mock_vs.init.side_effect = fake_init
70
+ mock_get_vs.return_value = mock_vs
71
+
72
+ notes_data = [{"title": "测试初始化", "content": "内容"}]
73
+ result = bulk_import_notes(notes_data, show_progress=False)
74
+
75
+ # 验证 init 被调用了
76
+ mock_vs.init.assert_called()
77
+ # 导入成功
78
+ assert result["imported"] == 1
79
+ ```
80
+
81
+ - [ ] **Step 2: 运行测试确认它失败**
82
+
83
+ Run: `uv run pytest tests/unit/test_bm25_batch.py::TestBulkImportBM25Integration::test_bulk_import_with_uninitialized_vectorstore -v`
84
+ Expected: FAIL — 当前代码直接访问 `vector_store.collection.add()`,不会调用 `init()`,mock 的 `collection` 仍为 None,会抛 `AttributeError`。
85
+
86
+ - [ ] **Step 3: 修复 performance.py — 在 collection.add() 前添加初始化**
87
+
88
+ 在 `jfox/performance.py` 第 266 行之前,添加初始化检查。修改 `# 批量添加到 ChromaDB` 注释后的代码块:
89
+
90
+ ```python
91
+ # 确保 VectorStore 已初始化
92
+ if vector_store.collection is None:
93
+ vector_store.init()
94
+
95
+ # 批量添加到 ChromaDB
96
+ vector_store.collection.add(
97
+ ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
98
+ )
99
+ ```
100
+
101
+ 完整上下文(`performance.py` 约第 254-268 行)修改后为:
102
+
103
+ ```python
104
+ # 批量添加到 ChromaDB
105
+ ids = [n.id for n in notes]
106
+ metadatas = [
107
+ {
108
+ "title": n.title,
109
+ "type": n.type.value,
110
+ "filepath": str(n.filepath),
111
+ "tags": ",".join(n.tags),
112
+ }
113
+ for n in notes
114
+ ]
115
+
116
+ # 确保 VectorStore 已初始化
117
+ if vector_store.collection is None:
118
+ vector_store.init()
119
+
120
+ vector_store.collection.add(
121
+ ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
122
+ )
123
+ ```
124
+
125
+ - [ ] **Step 4: 运行测试确认通过**
126
+
127
+ Run: `uv run pytest tests/unit/test_bm25_batch.py -v`
128
+ Expected: ALL PASS(包括新测试和原有测试)
129
+
130
+ - [ ] **Step 5: Commit**
131
+
132
+ ```bash
133
+ git add jfox/performance.py tests/unit/test_bm25_batch.py
134
+ git commit -m "fix: bulk_import_notes 中 VectorStore.collection 未初始化导致索引失败
135
+
136
+ performance.py 的 bulk_import_notes() 直接访问 vector_store.collection.add(),
137
+ 绕过了 add_note() 中的 init() 保护。在新会话中直接执行 bulk-import 时,
138
+ collection 为 None 导致 'NoneType' object has no attribute 'add' 错误。
139
+
140
+ 在 collection.add() 前添加 if vector_store.collection is None: vector_store.init()。
141
+
142
+ Closes #126 (修复 1/2)"
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Task 2: 让 index rebuild 同时重建 BM25 索引
148
+
149
+ **Files:**
150
+ - Modify: `jfox/cli.py:1701-1713`
151
+ - Test: `tests/unit/test_index_kb_param.py`(在已有文件中添加测试)
152
+
153
+ - [ ] **Step 1: 写失败测试 — 验证 rebuild 时也调用了 BM25 重建**
154
+
155
+ 在 `tests/unit/test_index_kb_param.py` 中添加测试:
156
+
157
+ ```python
158
+ class TestIndexRebuildIncludesBM25:
159
+ """验证 index rebuild 同时重建 BM25 索引"""
160
+
161
+ @patch("jfox.cli.note")
162
+ @patch("jfox.cli.get_bm25_index")
163
+ @patch("jfox.cli.Indexer")
164
+ @patch("jfox.cli.get_vector_store")
165
+ def test_rebuild_calls_bm25_rebuild(
166
+ self, mock_get_vs, mock_indexer_cls, mock_get_bm25, mock_note, tmp_path
167
+ ):
168
+ """index rebuild 应在 ChromaDB 重建后调用 BM25 rebuild_from_notes"""
169
+ from jfox.cli import _index_impl
170
+
171
+ # mock vector store
172
+ mock_vs = MagicMock()
173
+ mock_get_vs.return_value = mock_vs
174
+
175
+ # mock indexer
176
+ mock_indexer = MagicMock()
177
+ mock_indexer.index_all.return_value = 10
178
+ mock_indexer_cls.return_value = mock_indexer
179
+
180
+ # mock note.list_notes
181
+ mock_notes = [MagicMock() for _ in range(3)]
182
+ mock_note.list_notes.return_value = mock_notes
183
+
184
+ # mock BM25
185
+ mock_bm25 = MagicMock()
186
+ mock_bm25.rebuild_from_notes.return_value = True
187
+ mock_get_bm25.return_value = mock_bm25
188
+
189
+ _index_impl("rebuild", "text")
190
+
191
+ # 验证 ChromaDB 重建被调用
192
+ mock_indexer.index_all.assert_called_once()
193
+ # 验证 BM25 重建也被调用
194
+ mock_get_bm25.assert_called_once()
195
+ mock_bm25.rebuild_from_notes.assert_called_once_with(mock_notes)
196
+ ```
197
+
198
+ - [ ] **Step 2: 运行测试确认它失败**
199
+
200
+ Run: `uv run pytest tests/unit/test_index_kb_param.py::TestIndexRebuildIncludesBM25 -v`
201
+ Expected: FAIL — 当前 `rebuild` 分支不调用 BM25 重建。
202
+
203
+ - [ ] **Step 3: 修改 cli.py 的 rebuild 分支,增加 BM25 重建**
204
+
205
+ 修改 `jfox/cli.py` 第 1701-1713 行的 `rebuild` 分支。修改后:
206
+
207
+ ```python
208
+ elif action == "rebuild":
209
+ console.print("[yellow]Rebuilding index...[/yellow]")
210
+ count = indexer.index_all()
211
+
212
+ # 同时重建 BM25 索引
213
+ from . import note as note_module
214
+ from .bm25_index import get_bm25_index
215
+
216
+ bm25_index = get_bm25_index()
217
+ notes = note_module.list_notes(limit=10000)
218
+ bm25_success = bm25_index.rebuild_from_notes(notes)
219
+
220
+ result = {
221
+ "success": True,
222
+ "indexed": count,
223
+ "bm25_rebuilt": bm25_success,
224
+ "bm25_indexed": len(notes),
225
+ }
226
+
227
+ if output_format == "json":
228
+ print(output_json(result))
229
+ else:
230
+ console.print(f"[green]✓[/green] Indexed {count} notes")
231
+ if bm25_success:
232
+ console.print(f"[green]✓[/green] BM25 index rebuilt: {len(notes)} notes")
233
+ else:
234
+ console.print("[yellow]⚠[/yellow] ChromaDB rebuilt, but BM25 rebuild failed")
235
+ ```
236
+
237
+ - [ ] **Step 4: 运行测试确认通过**
238
+
239
+ Run: `uv run pytest tests/unit/test_index_kb_param.py -v`
240
+ Expected: ALL PASS
241
+
242
+ - [ ] **Step 5: 验证现有 rebuild-bm25 单独命令仍正常**
243
+
244
+ Run: `uv run pytest tests/unit/test_index_kb_param.py -v -k "not RebuildIncludesBM25"`
245
+ Expected: ALL PASS(原有测试不受影响)
246
+
247
+ - [ ] **Step 6: Commit**
248
+
249
+ ```bash
250
+ git add jfox/cli.py tests/unit/test_index_kb_param.py
251
+ git commit -m "fix: index rebuild 同时重建 BM25 索引
252
+
253
+ 之前 index rebuild 只重建 ChromaDB,用户需额外手动执行 rebuild-bm25。
254
+ 现在 rebuild 操作自动同时重建两个索引,输出中显示各自的重建状态。
255
+
256
+ Refs #126 (修复 2/2)"
257
+ ```
258
+
259
+ ---
260
+
261
+ ## 自查清单
262
+
263
+ | 检查项 | 状态 |
264
+ |--------|------|
265
+ | 修复 1: performance.py VectorStore 初始化 | Task 1 覆盖 |
266
+ | 修复 2: cli.py rebuild 含 BM25 | Task 2 覆盖 |
267
+ | 现有测试不受影响 | Task 1/2 的 Step 4 验证 |
268
+ | 无 placeholder / TBD | 已检查,每步都有完整代码 |
269
+ | 类型/方法签名一致 | `rebuild_from_notes(notes)` 在 bm25_index.py:369 和 cli.py 一致 |
270
+ | `rebuild-bm25` 独立命令仍可用 | Task 2 Step 5 验证,独立命令逻辑未被修改 |