jfox-cli 0.4.1__tar.gz → 0.4.3__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 (123) hide show
  1. jfox_cli-0.4.3/.claude/skills/release/SKILL.md +169 -0
  2. jfox_cli-0.4.3/.claude/skills/release/release_helper.py +337 -0
  3. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/CHANGELOG.md +30 -0
  4. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/PKG-INFO +1 -1
  5. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/__init__.py +1 -1
  6. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/cli.py +83 -1
  7. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/daemon/process.py +17 -6
  8. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/daemon/server.py +10 -3
  9. jfox_cli-0.4.3/jfox/model_downloader.py +224 -0
  10. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/performance.py +1 -1
  11. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/pyproject.toml +1 -1
  12. jfox_cli-0.4.3/scripts/download-model-intranet.sh +69 -0
  13. jfox_cli-0.4.3/skills-recommend/README.md +93 -0
  14. jfox_cli-0.4.3/skills-recommend/kimi-cli/jfox-common/SKILL.md +314 -0
  15. jfox_cli-0.4.3/skills-recommend/kimi-cli/jfox-ingest/SKILL.md +217 -0
  16. jfox_cli-0.4.3/skills-recommend/kimi-cli/jfox-organize/SKILL.md +169 -0
  17. jfox_cli-0.4.3/skills-recommend/kimi-cli/jfox-search/SKILL.md +111 -0
  18. jfox_cli-0.4.3/skills-recommend/kimi-cli/jfox-session-summary/SKILL.md +122 -0
  19. jfox_cli-0.4.3/tests/integration/test_model_download.py +105 -0
  20. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_daemon_process.py +4 -1
  21. jfox_cli-0.4.3/tests/unit/test_model_downloader.py +111 -0
  22. jfox_cli-0.4.3/tests/unit/test_release_helper.py +128 -0
  23. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/uv.lock +1 -1
  24. jfox_cli-0.4.1/skills-recommend/README.md +0 -66
  25. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/.claude/settings.local.json +0 -0
  26. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/.githooks/pre-push +0 -0
  27. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/.github/workflows/integration-test.yml +0 -0
  28. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/.github/workflows/publish.yml +0 -0
  29. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/.gitignore +0 -0
  30. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/.python-version +0 -0
  31. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/AGENTS.md +0 -0
  32. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/CLAUDE.md +0 -0
  33. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/DEVELOPMENT_PLAN.md +0 -0
  34. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/README.md +0 -0
  35. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/SESSION.md +0 -0
  36. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/SESSION_SUMMARY.md +0 -0
  37. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/installation.md +0 -0
  38. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-11-bulk-import-bm25-fix.md +0 -0
  39. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-11-edit-command.md +0 -0
  40. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-11-unify-format-option.md +0 -0
  41. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md +0 -0
  42. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-12-edit-content-file.md +0 -0
  43. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md +0 -0
  44. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md +0 -0
  45. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-12-fix-jfox-health-skill-kb-param.md +0 -0
  46. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-12-index-kb-param.md +0 -0
  47. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-12-lazy-import-perf.md +0 -0
  48. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-12-skill-redesign.md +0 -0
  49. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-14-sync-docs-daemon-show.md +0 -0
  50. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/plans/2026-04-15-session-summary-confirmation.md +0 -0
  51. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/specs/2026-04-03-bugfixes-design.md +0 -0
  52. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md +0 -0
  53. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md +0 -0
  54. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/specs/2026-04-14-show-command-design.md +0 -0
  55. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/superpowers/specs/2026-04-15-session-summary-confirmation-design.md +0 -0
  56. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/docs/troubleshooting.md +0 -0
  57. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jessica-jones-static-cable.md +0 -0
  58. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/__main__.py +0 -0
  59. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/bm25_index.py +0 -0
  60. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/config.py +0 -0
  61. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/daemon/__init__.py +0 -0
  62. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/daemon/__main__.py +0 -0
  63. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/daemon/client.py +0 -0
  64. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/embedding_backend.py +0 -0
  65. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/formatters.py +0 -0
  66. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/git_extractor.py +0 -0
  67. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/global_config.py +0 -0
  68. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/graph.py +0 -0
  69. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/indexer.py +0 -0
  70. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/kb_manager.py +0 -0
  71. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/models.py +0 -0
  72. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/note.py +0 -0
  73. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/search_engine.py +0 -0
  74. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/template.py +0 -0
  75. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/template_cli.py +0 -0
  76. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/jfox/vector_store.py +0 -0
  77. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/pytest.ini +0 -0
  78. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/run_full_test.ps1 +0 -0
  79. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/skills-recommend/claude-code/jfox-common/SKILL.md +0 -0
  80. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/skills-recommend/claude-code/jfox-ingest/SKILL.md +0 -0
  81. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/skills-recommend/claude-code/jfox-organize/SKILL.md +0 -0
  82. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/skills-recommend/claude-code/jfox-search/SKILL.md +0 -0
  83. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/skills-recommend/claude-code/jfox-session-summary/SKILL.md +0 -0
  84. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/COVERAGE_PLAN.md +0 -0
  85. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/MIGRATION.md +0 -0
  86. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/TESTS.md +0 -0
  87. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/conftest.py +0 -0
  88. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/integration/__init__.py +0 -0
  89. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/integration/test_backlinks.py +0 -0
  90. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/performance/__init__.py +0 -0
  91. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/performance/test_performance.py +0 -0
  92. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_advanced_features.py +0 -0
  93. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_cli_format.py +0 -0
  94. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_config_set_unit.py +0 -0
  95. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_config_unit.py +0 -0
  96. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_core_workflow.py +0 -0
  97. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_embedding_device.py +0 -0
  98. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_hybrid_search.py +0 -0
  99. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_integration.py +0 -0
  100. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_kb_current.py +0 -0
  101. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/test_suggest_links.py +0 -0
  102. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/__init__.py +0 -0
  103. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_bm25_batch.py +0 -0
  104. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_edit.py +0 -0
  105. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_format_unify.py +0 -0
  106. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_formatters.py +0 -0
  107. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_git_extractor.py +0 -0
  108. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_global_config.py +0 -0
  109. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_index_kb_param.py +0 -0
  110. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_indexer_clear_before_rebuild.py +0 -0
  111. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_indexer_verify.py +0 -0
  112. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_kb_manager.py +0 -0
  113. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_lazy_import.py +0 -0
  114. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_logging_config.py +0 -0
  115. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_show.py +0 -0
  116. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_template.py +0 -0
  117. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_template_cli.py +0 -0
  118. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/unit/test_vector_store_clear.py +0 -0
  119. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/utils/__init__.py +0 -0
  120. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/utils/assertions.py +0 -0
  121. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/utils/jfox_cli.py +0 -0
  122. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/utils/note_generator.py +0 -0
  123. {jfox_cli-0.4.1 → jfox_cli-0.4.3}/tests/utils/temp_kb.py +0 -0
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: release
3
+ description: Release a new version of jfox. Bumps version, generates CHANGELOG, creates PR and GitHub Release. Triggers on "发版", "release", "bump version", "发布版本".
4
+ ---
5
+
6
+ # Release Skill
7
+
8
+ 将 jfox 发版流程从多步手动操作简化为一条命令。覆盖版本号 bump → CHANGELOG → commit → PR → GitHub Release 全流程。
9
+
10
+ ## 用法
11
+
12
+ ```
13
+ /release 0.5.0 # 指定具体版本号
14
+ /release patch # bump patch: 0.4.1 → 0.4.2
15
+ /release minor # bump minor: 0.4.1 → 0.5.0
16
+ /release major # bump major: 0.4.1 → 1.0.0
17
+ ```
18
+
19
+ ## 执行流程
20
+
21
+ 严格按照以下步骤执行。每一步必须完成后再进入下一步。
22
+
23
+ ### Step 1: 前置校验
24
+
25
+ 运行以下检查,任何一项失败则立即停止并告知用户原因:
26
+
27
+ ```bash
28
+ # 1. 当前分支必须是 main
29
+ git branch --show-current
30
+ # 期望输出: main
31
+
32
+ # 2. 工作区必须干净
33
+ git status --porcelain
34
+ # 期望输出: 空
35
+
36
+ # 3. 不存在未合并的 bump 分支
37
+ git branch --list 'chore/bump-*'
38
+ # 期望输出: 空
39
+
40
+ # 4. 没有未合并的 bump PR
41
+ gh pr list --state open --head "chore/bump-*"
42
+ # 期望输出: 空
43
+ ```
44
+
45
+ ### Step 2: 运行辅助脚本(预览模式)
46
+
47
+ 用 `--dry-run` 先预览计算结果:
48
+
49
+ ```bash
50
+ uv run python .claude/skills/release/release_helper.py <version> --dry-run
51
+ ```
52
+
53
+ 解析 JSON 输出,提取 `current_version`、`new_version`、`changelog_preview`、`changelog_summary`。
54
+
55
+ ### Step 3: 展示变更摘要并等待确认
56
+
57
+ 向用户展示:
58
+
59
+ ```
60
+ 📦 Release 预览:
61
+ 当前版本: {current_version}
62
+ 新版本号: {new_version}
63
+ 变更摘要: {changelog_summary}
64
+
65
+ CHANGELOG 预览:
66
+ {changelog_preview}
67
+
68
+ 将修改的文件:
69
+ - pyproject.toml
70
+ - jfox/__init__.py
71
+ - uv.lock
72
+ - CHANGELOG.md
73
+ ```
74
+
75
+ **必须等待用户明确确认后才继续。** 如果用户拒绝或要求修改,停止流程。
76
+
77
+ ### Step 4: 运行辅助脚本(正式模式)
78
+
79
+ ```bash
80
+ uv run python .claude/skills/release/release_helper.py <version>
81
+ ```
82
+
83
+ 确认脚本退出码为 0。如果非 0,读取错误信息并告知用户,停止流程。
84
+
85
+ ### Step 5: Git 操作
86
+
87
+ ```bash
88
+ # 创建分支
89
+ git checkout -b chore/bump-version-{new_version}
90
+
91
+ # 暂存文件
92
+ git add pyproject.toml jfox/__init__.py uv.lock CHANGELOG.md
93
+
94
+ # 提交
95
+ git commit -m "chore: bump version to {new_version}"
96
+
97
+ # 推送
98
+ git push -u origin chore/bump-version-{new_version}
99
+ ```
100
+
101
+ ### Step 6: 创建 PR
102
+
103
+ 使用 CHANGELOG 内容作为 PR body:
104
+
105
+ ```bash
106
+ gh pr create \
107
+ --title "chore: bump version to {new_version}" \
108
+ --body "$(cat <<'EOF'
109
+ ## Summary
110
+ Bump version from {current_version} to {new_version}
111
+
112
+ {changelog_preview}
113
+
114
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)
115
+ EOF
116
+ )"
117
+ ```
118
+
119
+ 记录返回的 PR URL。
120
+
121
+ ### Step 7: 等待合并
122
+
123
+ 告知用户:
124
+
125
+ ```
126
+ PR 已创建: {PR_URL}
127
+ 请合并此 PR 后告知我,我将继续创建 GitHub Release。
128
+ ```
129
+
130
+ 等待用户确认 PR 已合并。
131
+
132
+ ### Step 8: 切回 main 并拉取最新代码
133
+
134
+ ```bash
135
+ git checkout main
136
+ git pull origin main
137
+ ```
138
+
139
+ ### Step 9: 创建 GitHub Release
140
+
141
+ ```bash
142
+ gh release create v{new_version} \
143
+ --title "v{new_version}" \
144
+ --notes "$(cat <<'EOF'
145
+ {changelog_preview}
146
+ EOF
147
+ )"
148
+ ```
149
+
150
+ 告知用户:
151
+
152
+ ```
153
+ Release v{new_version} 已创建!
154
+ GitHub Actions 将自动发布到 PyPI。
155
+ 可在 https://github.com/zhuxixi/jfox/actions 监控发布状态。
156
+ ```
157
+
158
+ ## 错误处理
159
+
160
+ - 脚本返回非零退出码 → 读取错误 JSON,展示给用户,停止流程
161
+ - git 操作失败 → 展示错误信息,建议用户手动修复
162
+ - PR 创建失败 → 检查是否已有同名 PR,或提示权限问题
163
+ - Release 创建失败 → 检查 tag 是否已存在,或提示权限问题
164
+
165
+ ## 注意事项
166
+
167
+ - **不使用 `--no-verify`**,保持 pre-commit hook 正常运行
168
+ - **始终在新分支操作**,不直接修改 main
169
+ - **每个确认点都必须等待**,不自动跳过
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ jfox release 辅助脚本
4
+
5
+ 处理版本号计算、文件更新、CHANGELOG 生成。
6
+ 输出 JSON 供 Claude 解析。
7
+
8
+ 用法:
9
+ python release_helper.py patch # bump patch
10
+ python release_helper.py minor # bump minor
11
+ python release_helper.py major # bump major
12
+ python release_helper.py 0.5.0 # 指定版本
13
+ python release_helper.py ... --dry-run # 只计算不修改文件
14
+ """
15
+ import json
16
+ import re
17
+ import subprocess
18
+ import sys
19
+ from datetime import date
20
+ from pathlib import Path
21
+
22
+ # 项目根目录(脚本位于 .claude/skills/release/,向上 3 级)
23
+ PROJECT_ROOT = Path(__file__).resolve().parents[3]
24
+ PYPROJECT_TOML = PROJECT_ROOT / "pyproject.toml"
25
+ INIT_PY = PROJECT_ROOT / "jfox" / "__init__.py"
26
+ CHANGELOG_MD = PROJECT_ROOT / "CHANGELOG.md"
27
+
28
+
29
+ def output_json(data: dict):
30
+ """输出 JSON 到 stdout"""
31
+ print(json.dumps(data, ensure_ascii=False))
32
+
33
+
34
+ def output_error(msg: str):
35
+ """输出错误 JSON 并退出"""
36
+ output_json({"error": msg})
37
+ sys.exit(1)
38
+
39
+
40
+ def read_current_version() -> str:
41
+ """从 pyproject.toml 读取当前版本号"""
42
+ if not PYPROJECT_TOML.exists():
43
+ output_error(f"未找到 {PYPROJECT_TOML}")
44
+ content = PYPROJECT_TOML.read_text(encoding="utf-8")
45
+ match = re.search(r'^version\s*=\s*"(\d+\.\d+\.\d+)"', content, re.MULTILINE)
46
+ if not match:
47
+ output_error(f"未在 {PYPROJECT_TOML} 中找到 version 字段")
48
+ return match.group(1)
49
+
50
+
51
+ def parse_bump(arg: str, current: str) -> str:
52
+ """解析版本参数,返回新版本号"""
53
+ # 尝试作为 bump 类型
54
+ if arg in ("patch", "minor", "major"):
55
+ parts = [int(x) for x in current.split(".")]
56
+ if arg == "patch":
57
+ parts[2] += 1
58
+ elif arg == "minor":
59
+ parts[1] += 1
60
+ parts[2] = 0
61
+ elif arg == "major":
62
+ parts[0] += 1
63
+ parts[1] = 0
64
+ parts[2] = 0
65
+ return f"{parts[0]}.{parts[1]}.{parts[2]}"
66
+
67
+ # 尝试作为 semver
68
+ if not re.match(r"^\d+\.\d+\.\d+$", arg):
69
+ output_error(f"无效的版本号或 bump 类型: {arg}(期望 patch/minor/major 或 X.Y.Z)")
70
+
71
+ # 确保新版本 > 当前版本
72
+ new_parts = [int(x) for x in arg.split(".")]
73
+ cur_parts = [int(x) for x in current.split(".")]
74
+ if new_parts <= cur_parts:
75
+ output_error(f"新版本 {arg} 不大于当前版本 {current}")
76
+
77
+ return arg
78
+
79
+
80
+ def update_files(new_version: str, current_version: str):
81
+ """更新 pyproject.toml、__init__.py、uv.lock"""
82
+ if not PYPROJECT_TOML.exists():
83
+ output_error(f"未找到 {PYPROJECT_TOML}")
84
+ if not INIT_PY.exists():
85
+ output_error(f"未找到 {INIT_PY}")
86
+
87
+ # 先读取备份
88
+ toml_content = PYPROJECT_TOML.read_text(encoding="utf-8")
89
+ init_content = INIT_PY.read_text(encoding="utf-8")
90
+
91
+ # 更新 pyproject.toml
92
+ new_toml = re.sub(
93
+ r'^version\s*=\s*"\d+\.\d+\.\d+"',
94
+ f'version = "{new_version}"',
95
+ toml_content,
96
+ flags=re.MULTILINE,
97
+ )
98
+ PYPROJECT_TOML.write_text(new_toml, encoding="utf-8")
99
+
100
+ # 更新 __init__.py
101
+ new_init = re.sub(
102
+ r'__version__\s*=\s*"\d+\.\d+\.\d+"',
103
+ f'__version__ = "{new_version}"',
104
+ init_content,
105
+ )
106
+ INIT_PY.write_text(new_init, encoding="utf-8")
107
+
108
+ # uv.lock — 如果失败则回滚
109
+ result = subprocess.run(
110
+ ["uv", "lock"],
111
+ cwd=str(PROJECT_ROOT),
112
+ capture_output=True,
113
+ text=True,
114
+ encoding="utf-8",
115
+ errors="replace",
116
+ )
117
+ if result.returncode != 0:
118
+ # 回滚
119
+ PYPROJECT_TOML.write_text(toml_content, encoding="utf-8")
120
+ INIT_PY.write_text(init_content, encoding="utf-8")
121
+ output_error(f"uv lock 失败,已回滚文件变更: {result.stderr}")
122
+
123
+
124
+ def get_last_tag() -> str:
125
+ """获取最新的 git tag"""
126
+ result = subprocess.run(
127
+ ["git", "describe", "--tags", "--abbrev=0"],
128
+ cwd=str(PROJECT_ROOT),
129
+ capture_output=True,
130
+ text=True,
131
+ encoding="utf-8",
132
+ errors="replace",
133
+ )
134
+ if result.returncode != 0:
135
+ return "" # 没有 tag
136
+ return result.stdout.strip()
137
+
138
+
139
+ def parse_commits(last_tag: str) -> list[dict]:
140
+ """解析 last_tag..HEAD 之间的 commit,返回分类后的条目列表"""
141
+ if last_tag:
142
+ range_spec = f"{last_tag}..HEAD"
143
+ else:
144
+ range_spec = "HEAD"
145
+
146
+ # -c core.quotepath=false 确保 git 输出中文不被转义
147
+ result = subprocess.run(
148
+ ["git", "-c", "core.quotepath=false", "-c", "i18n.logoutputencoding=utf-8",
149
+ "log", range_spec, "--format=%s"],
150
+ cwd=str(PROJECT_ROOT),
151
+ capture_output=True,
152
+ text=True,
153
+ encoding="utf-8",
154
+ errors="replace",
155
+ )
156
+ if result.returncode != 0 or not result.stdout:
157
+ return []
158
+
159
+ entries = []
160
+ seen = set() # 去重(merge commit 和 squash 可能重复)
161
+
162
+ for line in result.stdout.strip().split("\n"):
163
+ if not line.strip():
164
+ continue
165
+
166
+ # 跳过 bump version 的 commit
167
+ if "bump version" in line.lower():
168
+ continue
169
+
170
+ # 跳过纯 merge commit
171
+ if line.startswith("Merge ") and not any(
172
+ x in line for x in ["feat", "fix", "refactor", "docs", "chore", "perf"]
173
+ ):
174
+ continue
175
+
176
+ # 解析 conventional commit: type(scope): message (#PR)
177
+ match = re.match(
178
+ r"^(feat|fix|refactor|docs|chore|perf)(?:\(([^)]+)\))?:\s*(.+?)(?:\s*\(#(\d+)\))?$",
179
+ line,
180
+ )
181
+ if match:
182
+ entry = {
183
+ "type": match.group(1),
184
+ "scope": match.group(2) or "",
185
+ "message": match.group(3).strip(),
186
+ "pr": int(match.group(4)) if match.group(4) else None,
187
+ }
188
+ else:
189
+ # 非 conventional commit 格式
190
+ pr_match = re.search(r"\(#(\d+)\)", line)
191
+ entry = {
192
+ "type": "other",
193
+ "scope": "",
194
+ "message": line.strip(),
195
+ "pr": int(pr_match.group(1)) if pr_match else None,
196
+ }
197
+
198
+ # 去重 key
199
+ key = (entry["type"], entry["scope"], entry["message"])
200
+ if key not in seen:
201
+ seen.add(key)
202
+ entries.append(entry)
203
+
204
+ return entries
205
+
206
+
207
+ def generate_changelog(new_version: str, current_version: str, entries: list[dict], last_tag: str = "") -> str:
208
+ """生成 CHANGELOG Markdown 内容"""
209
+ today = date.today().isoformat()
210
+
211
+ # 按 type 分类
212
+ features = [e for e in entries if e["type"] == "feat"]
213
+ fixes = [e for e in entries if e["type"] == "fix"]
214
+ perfs = [e for e in entries if e["type"] == "perf"]
215
+ changes = [e for e in entries if e["type"] not in ("feat", "fix", "perf")]
216
+
217
+ lines = [f"## [{new_version}] - {today}", ""]
218
+
219
+ def format_entry(e: dict) -> str:
220
+ scope = f"**{e['scope']}**: " if e["scope"] else ""
221
+ pr = f" (#{e['pr']})" if e["pr"] else ""
222
+ return f"- {scope}{e['message']}{pr}"
223
+
224
+ if features:
225
+ lines.append("### Features")
226
+ lines.extend(format_entry(e) for e in features)
227
+ lines.append("")
228
+
229
+ if fixes:
230
+ lines.append("### Fixes")
231
+ lines.extend(format_entry(e) for e in fixes)
232
+ lines.append("")
233
+
234
+ if perfs:
235
+ lines.append("### Performance")
236
+ lines.extend(format_entry(e) for e in perfs)
237
+ lines.append("")
238
+
239
+ if changes:
240
+ lines.append("### Changes")
241
+ lines.extend(format_entry(e) for e in changes)
242
+ lines.append("")
243
+
244
+ # 底部比较链接
245
+ tag_prev = last_tag.lstrip("v") if last_tag else current_version
246
+ lines.append(
247
+ f"[{new_version}]: https://github.com/zhuxixi/jfox/compare/"
248
+ f"v{tag_prev}...v{new_version}"
249
+ )
250
+
251
+ return "\n".join(lines)
252
+
253
+
254
+ def update_changelog_file(new_changelog: str):
255
+ """将新条目插入 CHANGELOG.md 头部"""
256
+ if not CHANGELOG_MD.exists():
257
+ content = "# Changelog\n\n"
258
+ else:
259
+ content = CHANGELOG_MD.read_text(encoding="utf-8")
260
+
261
+ # 在第一个 ## 之前插入
262
+ insert_pos = content.find("\n## ")
263
+ if insert_pos == -1:
264
+ content = content.rstrip("\n") + "\n\n" + new_changelog + "\n"
265
+ else:
266
+ content = content[: insert_pos + 1] + new_changelog + "\n\n" + content[insert_pos + 1 :]
267
+
268
+ CHANGELOG_MD.write_text(content, encoding="utf-8")
269
+
270
+
271
+ def summarize_entries(entries: list[dict]) -> str:
272
+ """生成条目摘要"""
273
+ counts = {}
274
+ for e in entries:
275
+ t = e["type"]
276
+ if t == "feat":
277
+ counts["feature"] = counts.get("feature", 0) + 1
278
+ elif t == "fix":
279
+ counts["fix"] = counts.get("fix", 0) + 1
280
+ else:
281
+ counts["change"] = counts.get("change", 0) + 1
282
+
283
+ parts = []
284
+ for label in ("feature", "fix", "change"):
285
+ if label in counts:
286
+ plural = {"feature": "features", "fix": "fixes", "change": "changes"}
287
+ name = plural[label] if counts[label] > 1 else label
288
+ parts.append(f"{counts[label]} {name}")
289
+ return ", ".join(parts) if parts else "0 changes"
290
+
291
+
292
+ def main():
293
+ if len(sys.argv) < 2:
294
+ output_error("用法: release_helper.py <version|patch|minor|major> [--dry-run]")
295
+
296
+ version_arg = sys.argv[1]
297
+ dry_run = "--dry-run" in sys.argv
298
+
299
+ # 1. 读取当前版本
300
+ current_version = read_current_version()
301
+
302
+ # 2. 计算新版本
303
+ new_version = parse_bump(version_arg, current_version)
304
+
305
+ # 3. 解析 commits
306
+ last_tag = get_last_tag()
307
+ entries = parse_commits(last_tag)
308
+
309
+ # 4. 生成 CHANGELOG
310
+ changelog = generate_changelog(new_version, current_version, entries, last_tag)
311
+
312
+ result = {
313
+ "current_version": current_version,
314
+ "new_version": new_version,
315
+ "last_tag": last_tag,
316
+ "changelog_preview": changelog,
317
+ "changelog_entries": entries,
318
+ "changelog_summary": summarize_entries(entries),
319
+ "files_modified": ["pyproject.toml", "jfox/__init__.py", "uv.lock", "CHANGELOG.md"],
320
+ }
321
+
322
+ if dry_run:
323
+ output_json(result)
324
+ return
325
+
326
+ # 5. 更新文件
327
+ update_files(new_version, current_version)
328
+
329
+ # 6. 更新 CHANGELOG
330
+ update_changelog_file(changelog)
331
+
332
+ # 7. 输出结果
333
+ output_json(result)
334
+
335
+
336
+ if __name__ == "__main__":
337
+ main()
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to jfox-cli will be documented in this file.
4
4
 
5
+ ## [0.4.3] - 2026-04-28
6
+
7
+ ### Features
8
+ - 内网模型自动下载(3步降级重试链) (#173)
9
+
10
+ ### Fixes
11
+ - **daemon**: eliminate deprecation warnings in daemon log (#171)
12
+
13
+ [0.4.3]: https://github.com/zhuxixi/jfox/compare/v0.4.2...v0.4.3
14
+
15
+ ## [0.4.2] - 2026-04-22
16
+
17
+ ### Features
18
+ - add Kimi CLI skill collection (#166)
19
+ - **skills**: add release skill with full workflow instructions
20
+ - **skills**: add release helper script with version bump and CHANGELOG generation
21
+
22
+ ### Fixes
23
+ - **lint**: remove unused pytest import
24
+ - **skills**: improve git Chinese encoding and fix pluralization in release helper
25
+ - **skills**: address code review issues in release helper
26
+ - **cli**: list 命令 table 输出显示完整 18 位笔记 ID
27
+
28
+ ### Changes
29
+ - style(lint): format test_release_helper.py with black
30
+ - Merge pull request #167 from zhuxixi/feat/kimi-cli-skills
31
+ - Merge pull request #165 from zhuxixi/fix-list-id-truncation
32
+
33
+ [0.4.2]: https://github.com/zhuxixi/jfox/compare/v0.4.1...v0.4.2
34
+
5
35
  ## [0.2.0] - 2026-04-13
6
36
 
7
37
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jfox-cli
3
- Version: 0.4.1
3
+ Version: 0.4.3
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.4.1"
3
+ __version__ = "0.4.3"
4
4
  __author__ = "User"
5
5
  __email__ = "user@example.com"
@@ -88,6 +88,10 @@ def _main(
88
88
  # 添加子命令
89
89
  app.add_typer(template_app, name="template", help="Manage note templates")
90
90
 
91
+ # Model 下载子命令
92
+ model_app = typer.Typer(name="model", help="模型管理")
93
+ app.add_typer(model_app, name="model", help="模型管理")
94
+
91
95
  console = Console(legacy_windows=False)
92
96
 
93
97
 
@@ -787,7 +791,7 @@ def _list_impl(
787
791
 
788
792
  for n in notes:
789
793
  created_str = n.created.strftime("%Y-%m-%d") if n.created else ""
790
- table.add_row(n.id[:14], n.title[:40], n.type.value, created_str)
794
+ table.add_row(n.id, n.title[:40], n.type.value, created_str)
791
795
 
792
796
  console.print(table)
793
797
  elif output_format == "tree":
@@ -2674,6 +2678,84 @@ def daemon(
2674
2678
  raise typer.Exit(1)
2675
2679
 
2676
2680
 
2681
+ def _download_impl(
2682
+ model: Optional[str],
2683
+ force: bool = False,
2684
+ ) -> dict:
2685
+ """下载模型实现(可复用)"""
2686
+ from .embedding_backend import EmbeddingBackend
2687
+ from .model_downloader import ModelDownloader
2688
+
2689
+ # 解析模型名
2690
+ if model is None or model == "auto":
2691
+ backend = EmbeddingBackend()
2692
+ device = backend._resolve_device()
2693
+ model = backend._resolve_model_name(device)
2694
+
2695
+ downloader = ModelDownloader(model)
2696
+
2697
+ if force and downloader._check_cached():
2698
+ import shutil
2699
+
2700
+ shutil.rmtree(downloader._model_cache, ignore_errors=True)
2701
+
2702
+ ok = downloader.ensure_cached()
2703
+ return {
2704
+ "model": model,
2705
+ "success": ok,
2706
+ "cache_dir": str(downloader._model_cache),
2707
+ "instructions": downloader.get_manual_instructions() if not ok else "",
2708
+ }
2709
+
2710
+
2711
+ @model_app.command("download")
2712
+ def download(
2713
+ model: Optional[str] = typer.Option(
2714
+ None, "--model", "-m", help="模型名(默认从配置读取,auto 则按设备自动选择)"
2715
+ ),
2716
+ force: bool = typer.Option(False, "--force", "-f", help="强制重新下载(覆盖已有缓存)"),
2717
+ kb: Optional[str] = typer.Option(
2718
+ None, "--kb", "-k", help="目标知识库名称(模型下载不依赖知识库)"
2719
+ ),
2720
+ output_format: str = typer.Option("table", "--format", help="输出格式: json, table"),
2721
+ json_output: bool = typer.Option(False, "--json", help="JSON 输出快捷方式"),
2722
+ ):
2723
+ """
2724
+ 手动下载 embedding 模型
2725
+
2726
+ 自动尝试 3 种下载方式(huggingface_hub → 镜像站 → curl)。
2727
+ 通常不需要手动调用,daemon start 会自动执行。
2728
+
2729
+ 示例:
2730
+
2731
+ jfox model download # 下载默认模型
2732
+ jfox model download --model bge-m3 # 下载指定模型
2733
+ jfox model download --force # 强制重新下载
2734
+ jfox model download --json # JSON 输出
2735
+ """
2736
+ # kb 参数保持 CLI 一致性(模型下载不依赖知识库)
2737
+ _ = kb
2738
+
2739
+ if json_output:
2740
+ output_format = "json"
2741
+
2742
+ console.print(f"[yellow]准备下载模型: {model or 'auto'}[/yellow]")
2743
+
2744
+ result = _download_impl(model=model, force=force)
2745
+
2746
+ if output_format == "json":
2747
+ console.print(output_json(result))
2748
+ else:
2749
+ if result["success"]:
2750
+ console.print(f"[green]✓ 模型下载完成: {result['model']}[/green]")
2751
+ else:
2752
+ console.print("[red]✗ 模型下载失败[/red]")
2753
+ console.print(Panel(result["instructions"], title="手动下载"))
2754
+
2755
+ if not result["success"]:
2756
+ raise typer.Exit(1)
2757
+
2758
+
2677
2759
  # 入口点
2678
2760
  def main():
2679
2761
  """CLI 入口点"""