qi-coding-agent 0.2.0__tar.gz → 0.2.1__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 (251) hide show
  1. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/PKG-INFO +2 -1
  2. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/README.md +1 -0
  3. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/models.md +1 -0
  4. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/packages.md +28 -21
  5. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/pyproject.toml +1 -1
  6. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/pyproject.toml +1 -1
  7. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/pyproject.toml +1 -1
  8. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/pyproject.toml +1 -1
  9. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/__init__.py +1 -1
  10. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/auth.py +39 -3
  11. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/cli.py +25 -7
  12. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/llm.py +37 -3
  13. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/runtime.py +2 -3
  14. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/settings.py +16 -0
  15. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/tui.py +7 -1
  16. qi_coding_agent-0.2.1/tests/test_auth_key.py +96 -0
  17. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_install.py +33 -0
  18. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_trust_scope.py +11 -0
  19. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_no_model.py +25 -0
  20. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/.github/workflows/publish.yml +0 -0
  21. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/.gitignore +0 -0
  22. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/.pi-lens.json +0 -0
  23. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/LICENSE +0 -0
  24. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/THIRD_PARTY_NOTICES.md +0 -0
  25. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/PLAN.md +0 -0
  26. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/agent-config-design.md +0 -0
  27. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/bash-allowlist.md +0 -0
  28. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/development.md +0 -0
  29. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/dispatcher.md +0 -0
  30. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/extensions-design.md +0 -0
  31. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/internals.md +0 -0
  32. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/overview.md +0 -0
  33. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/pi-alignment.md +0 -0
  34. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/plugins.md +0 -0
  35. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/web.md +0 -0
  36. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/cli.md +0 -0
  37. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/compaction.md +0 -0
  38. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/configuration.md +0 -0
  39. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/containerization.md +0 -0
  40. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/docs.json +0 -0
  41. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/environment-variables.md +0 -0
  42. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/extensions.md +0 -0
  43. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/how-qi-works.md +0 -0
  44. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/index.md +0 -0
  45. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/json.md +0 -0
  46. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/keybindings.md +0 -0
  47. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/providers.md +0 -0
  48. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/quickstart.md +0 -0
  49. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/security.md +0 -0
  50. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/session-format.md +0 -0
  51. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/sessions.md +0 -0
  52. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/settings.md +0 -0
  53. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/skills.md +0 -0
  54. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/slash-commands.md +0 -0
  55. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/themes.md +0 -0
  56. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/tui.md +0 -0
  57. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/usage.md +0 -0
  58. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/README.md +0 -0
  59. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-analyst/agent.md +0 -0
  60. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-analyst/assets/review-rules.md +0 -0
  61. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-analyst/mcp.json +0 -0
  62. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-analyst/skills/code-review-checklist/SKILL.md +0 -0
  63. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-reviewer/agent.md +0 -0
  64. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/data-analyst/agent.md +0 -0
  65. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/doc-writer/agent.md +0 -0
  66. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/doc-writer/assets/doc-style.md +0 -0
  67. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/general/agent.md +0 -0
  68. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/test-runner/agent.md +0 -0
  69. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/writer/agent.md +0 -0
  70. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/extensions/README.md +0 -0
  71. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/extensions/hello/README.md +0 -0
  72. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/extensions/hello/extension.py +0 -0
  73. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/README.md +0 -0
  74. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/qi_agents/__init__.py +0 -0
  75. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/qi_agents/discovery.py +0 -0
  76. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/qi_agents/subagent.py +0 -0
  77. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/README.md +0 -0
  78. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/__init__.py +0 -0
  79. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/client.py +0 -0
  80. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/config.py +0 -0
  81. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/direct.py +0 -0
  82. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/proxy.py +0 -0
  83. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/role.py +0 -0
  84. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/servers.py +0 -0
  85. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/README.md +0 -0
  86. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/__init__.py +0 -0
  87. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/agui.py +0 -0
  88. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/app.py +0 -0
  89. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/browse.py +0 -0
  90. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/files.py +0 -0
  91. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/schemas.py +0 -0
  92. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/security.py +0 -0
  93. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/serve.py +0 -0
  94. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/state.py +0 -0
  95. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/static/assets/index-96aT_6NK.js +0 -0
  96. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/static/assets/index-DePFgopp.css +0 -0
  97. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/static/index.html +0 -0
  98. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/index.html +0 -0
  99. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/package-lock.json +0 -0
  100. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/package.json +0 -0
  101. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/scripts/check-design.mjs +0 -0
  102. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/scripts/check-dsh-tokens.mjs +0 -0
  103. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/App.tsx +0 -0
  104. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/api/client.test.ts +0 -0
  105. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/api/client.ts +0 -0
  106. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/api/contract.test.ts +0 -0
  107. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/api/types.ts +0 -0
  108. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/commands.tsx +0 -0
  109. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/AgentMenu.tsx +0 -0
  110. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/CommandMenu.tsx +0 -0
  111. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/ConfirmDialog.tsx +0 -0
  112. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/DirPicker.tsx +0 -0
  113. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/Dock.tsx +0 -0
  114. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/FilePanel.tsx +0 -0
  115. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/ModelMenu.test.ts +0 -0
  116. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/ModelMenu.tsx +0 -0
  117. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/ProjectMenu.tsx +0 -0
  118. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/Rail.tsx +0 -0
  119. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/RowMenu.tsx +0 -0
  120. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/Rows.tsx +0 -0
  121. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/Settings.tsx +0 -0
  122. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/SidebarSection.tsx +0 -0
  123. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/icons.tsx +0 -0
  124. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/main.tsx +0 -0
  125. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/markdown/cjk.ts +0 -0
  126. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/markdown/markdown.test.ts +0 -0
  127. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/markdown/parse.ts +0 -0
  128. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/markdown/render.tsx +0 -0
  129. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/projects.test.ts +0 -0
  130. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/projects.ts +0 -0
  131. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/route.test.ts +0 -0
  132. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/route.ts +0 -0
  133. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/stats.test.ts +0 -0
  134. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/stats.ts +0 -0
  135. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/turn.test.ts +0 -0
  136. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/turn.ts +0 -0
  137. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/theme/app.css +0 -0
  138. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/theme/theme.ts +0 -0
  139. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/theme/tokens.css +0 -0
  140. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/vite-env.d.ts +0 -0
  141. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/tsconfig.json +0 -0
  142. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/vite.config.ts +0 -0
  143. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/vitest.config.ts +0 -0
  144. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/pyrightconfig.json +0 -0
  145. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/scripts/check_versions.py +0 -0
  146. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/scripts/e2e_ui_model.py +0 -0
  147. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/scripts/prune_empty_sessions.py +0 -0
  148. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/abort.py +0 -0
  149. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/compaction.py +0 -0
  150. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/config.py +0 -0
  151. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/export_html.py +0 -0
  152. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/extensions.py +0 -0
  153. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/loader.py +0 -0
  154. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/model_catalog.py +0 -0
  155. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/models.py +0 -0
  156. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/packages.py +0 -0
  157. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/paths.py +0 -0
  158. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/pep723.py +0 -0
  159. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/presets.py +0 -0
  160. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/prompt.py +0 -0
  161. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/registry.py +0 -0
  162. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/runner.py +0 -0
  163. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/session.py +0 -0
  164. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/share.py +0 -0
  165. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/system_prompt.py +0 -0
  166. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/theme.py +0 -0
  167. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/themes/README.md +0 -0
  168. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/themes/dark.json +0 -0
  169. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/themes/light.json +0 -0
  170. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/titling.py +0 -0
  171. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/tools/__init__.py +0 -0
  172. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/tools/shell.py +0 -0
  173. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/trust.py +0 -0
  174. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/workspaces.py +0 -0
  175. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/conftest.py +0 -0
  176. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_branch_summary_prompt.py +0 -0
  177. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_browse.py +0 -0
  178. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_init.py +0 -0
  179. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_pi_alignment.py +0 -0
  180. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_run.py +0 -0
  181. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_tool_flags.py +0 -0
  182. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_compaction.py +0 -0
  183. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_compaction_events.py +0 -0
  184. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_config.py +0 -0
  185. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_config_resources.py +0 -0
  186. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_contract_p0.py +0 -0
  187. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_docs_injection.py +0 -0
  188. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_doctor_extensions.py +0 -0
  189. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_example_extension.py +0 -0
  190. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_export_html.py +0 -0
  191. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_alignment.py +0 -0
  192. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_bus.py +0 -0
  193. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_cli_commands.py +0 -0
  194. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_commands.py +0 -0
  195. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_deps.py +0 -0
  196. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_entry_points.py +0 -0
  197. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_events.py +0 -0
  198. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_exclusions.py +0 -0
  199. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_flags.py +0 -0
  200. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_interop.py +0 -0
  201. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_messages.py +0 -0
  202. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_model.py +0 -0
  203. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_resolvers.py +0 -0
  204. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_run_agent.py +0 -0
  205. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_runner_events.py +0 -0
  206. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_session.py +0 -0
  207. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_tools.py +0 -0
  208. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_ui.py +0 -0
  209. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extensions.py +0 -0
  210. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_interrupt.py +0 -0
  211. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_litellm_offline.py +0 -0
  212. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_missing_extension_hints.py +0 -0
  213. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_model_catalog.py +0 -0
  214. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_model_thinking_levels.py +0 -0
  215. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_package_resource_toggle.py +0 -0
  216. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_packages.py +0 -0
  217. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_pep723.py +0 -0
  218. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_presets.py +0 -0
  219. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_prune_empty_sessions.py +0 -0
  220. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_agents_extension.py +0 -0
  221. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_config.py +0 -0
  222. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_direct.py +0 -0
  223. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_http.py +0 -0
  224. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_proxy.py +0 -0
  225. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_role.py +0 -0
  226. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_stdio.py +0 -0
  227. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_runner_core.py +0 -0
  228. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_session_model_entries.py +0 -0
  229. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_session_store.py +0 -0
  230. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_session_tree.py +0 -0
  231. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_settings_skills.py +0 -0
  232. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_share.py +0 -0
  233. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_shell.py +0 -0
  234. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_skill_commands.py +0 -0
  235. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_streaming.py +0 -0
  236. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_system_prompt.py +0 -0
  237. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_theme.py +0 -0
  238. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_thinking.py +0 -0
  239. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_thinking_budgets.py +0 -0
  240. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_third_party_notices.py +0 -0
  241. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_titling.py +0 -0
  242. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tool_execution_mode.py +0 -0
  243. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_extension_render.py +0 -0
  244. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_model_restore.py +0 -0
  245. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_smoke.py +0 -0
  246. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_style.py +0 -0
  247. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_web_api.py +0 -0
  248. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_web_extensions_api.py +0 -0
  249. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_web_files.py +0 -0
  250. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_workspaces.py +0 -0
  251. {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: qi-coding-agent
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: 编码 agent 框架:单 agent core + 扩展(MCP / 多 agent / web)
5
5
  Project-URL: Homepage, https://github.com/viewnah/qi
6
6
  Project-URL: Repository, https://github.com/viewnah/qi
@@ -70,6 +70,7 @@ qi # 裸 qi 就是界面(装完直接敲)
70
70
  ```text
71
71
  /login # 选 provider(预置都在列表里)→ 遮罩输入 API key → 写 ~/.qi/agent/auth.json
72
72
  /model # 选模型(只列解析得出凭证的;刚才没有模型时 /login 会顺带选中第一个)
73
+ # 选择器里按 ctrl+s = **存为默认**;不按就只对本次会话生效
73
74
  ```
74
75
 
75
76
  然后直接说你要做什么。三种跑法:
@@ -40,6 +40,7 @@ qi # 裸 qi 就是界面(装完直接敲)
40
40
  ```text
41
41
  /login # 选 provider(预置都在列表里)→ 遮罩输入 API key → 写 ~/.qi/agent/auth.json
42
42
  /model # 选模型(只列解析得出凭证的;刚才没有模型时 /login 会顺带选中第一个)
43
+ # 选择器里按 ctrl+s = **存为默认**;不按就只对本次会话生效
43
44
  ```
44
45
 
45
46
  然后直接说你要做什么。三种跑法:
@@ -143,4 +143,5 @@ models.json: $QI_AGENT_CONFIG(env 指定文件,最高)
143
143
  | 模型不在 `/model` 或 `ctrl+p` 清单里 | 清单只列**解析得出凭证**的 provider —— 跑 `qi --list-models` 看 `credential` 列 |
144
144
  | "我明明设了环境变量" | `qi doctor` 会给出凭证**来源**(`auth(...)` / `env:XXX` / `config:...`),区分"没读到"与"读到了但为空" |
145
145
  | provider 拒收 `reasoning_effort` | qi 会自动去掉参数重试并在 footer 提示一次;也可把该模型的 `reasoning` 改成 `false` |
146
+ | 回合失败出现 `'ascii' codec can't encode` | **API key 里有非 ASCII 字符**(多半是把一段中文提示误粘进了密钥框)。现在 qi 在请求前就报“API key 含非 ASCII 字符(来源:…)”;修:`qi auth list` 看来源 → 重新 `qi auth login <provider>` / 改环境变量 / 改 `models.json` 的 `apiKey` |
146
147
  | 相容端点返回 4xx | 先确认 `api` 形态选对(`openai-completions` vs `openai-responses` vs `anthropic-messages`) |
@@ -88,41 +88,48 @@ uv tool install qi-coding-agent --with "qi-mcp>=0.2"
88
88
 
89
89
  ## 5. 怎么读输出
90
90
 
91
- 装好了但没声明(这份环境里三个扩展都是 editable 装进来的):
91
+ 装好了但没声明(这份环境里三个扩展都是 pip 装进来的):
92
92
 
93
93
  ```text
94
94
  $ qi list
95
- 扩展
96
- ┏━━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓
97
- ┃ 扩展 ┃ 通道 ┃ 版本 ┃ 来源 ┃ 声明 ┃
98
- ┡━━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩
99
- │ qi-mcp │ pip │ 0.1.0 │ qi-mcp 0.1.0 · user │ 否 │
100
- │ qi-agents │ pip │ 0.1.0 │ qi-agents 0.1.0 · user │ 否 │
101
- │ qi-web │ pip │ 0.1.0 │ qi-web 0.1.0 · user │ 否 │
102
- └───────────┴──────┴───────┴────────────────────────┴──────┘
103
- 已装但未声明(3): qi-mcp、qi-agents、qi-web —— 写进 settings.packages 才能在 uv
104
- tool 重建后补回
95
+ 扩展
96
+ ┏━━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓
97
+ ┃ 扩展 ┃ 通道 ┃ 版本 ┃ 来源 ┃ 声明 ┃
98
+ ┡━━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩
99
+ │ qi-mcp │ pip │ 0.1.0 │ qi-mcp 0.1.0 · env │ 否 │
100
+ │ qi-agents │ pip │ 0.1.0 │ qi-agents 0.1.0 · env │ 否 │
101
+ │ qi-web │ pip │ 0.1.0 │ qi-web 0.1.0 · env │ 否 │
102
+ └───────────┴──────┴───────┴───────────────────────┴──────┘
103
+ 已装但未声明(3): qi-mcp、qi-agents、qi-web —— pip 扩展只有声明了才会加载;
104
+ 写进 settings.packages 才生效(也是 uv tool 重建后能补回的依据)
105
105
  ```
106
106
 
107
- 声明里有一条装不上、还有一条认不出时(`packages: ["pip:qi-todo", "qi-agents", "git+https://host/repo"]`):
107
+ > **两个「作用域」别混**:`来源` 列的 `· env` 说的是**装在哪** —— pip 扩展都装进 qi 的
108
+ > **同一个解释器环境**,所以固定是 `env`(没有“项目一份 / 全局一份”)。`声明` 列才是
109
+ > **哪一级 `settings.packages` 写了它**:`user` = `~/.qi/agent/settings.json`,
110
+ > `project` = `<git 根>/.qi/settings.json`(`qi install -l` 写的就是后者)。
111
+
112
+ 声明里有一条装不上、一条认不出,并且同一个包两级都声明时:
108
113
 
109
114
  ```text
110
115
  扩展
111
- ┏━━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
112
- ┃ 扩展 ┃ 通道 ┃ 版本 ┃ 来源 ┃ 声明 ┃
113
- ┡━━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
114
- │ qi-mcp │ pip │ 0.1.0 │ qi-mcp 0.1.0 · user │ 否 │
115
- │ qi-agents │ pip │ 0.1.0 │ qi-agents 0.1.0 · user │ 是 │
116
- │ qi-web │ pip │ 0.1.0 │ qi-web 0.1.0 · user │ 否 │
117
- │ qi-todo │ pip │ — │ pip:qi-todo · settings:user │ 未安装 │
118
- └───────────┴──────┴───────┴─────────────────────────────┴────────┘
116
+ ┏━━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
117
+ ┃ 扩展 ┃ 通道 ┃ 版本 ┃ 来源 ┃ 声明 ┃
118
+ ┡━━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
119
+ │ qi-mcp │ pip │ 0.1.0 │ qi-mcp 0.1.0 · env │ 否 │
120
+ │ qi-agents │ pip │ 0.1.0 │ qi-agents 0.1.0 · env │ 是(user + project) │
121
+ │ qi-web │ pip │ 0.1.0 │ qi-web 0.1.0 · env │ 否 │
122
+ │ qi-todo │ pip │ — │ pip:qi-todo · settings:user │ 未安装 │
123
+ └───────────┴──────┴───────┴─────────────────────────────┴────────────────────┘
119
124
  包声明:
120
125
  ✗ 声明了但没装: pip:qi-todo (settings:user)
121
126
  qi install "qi-todo"
122
127
  uv tool install qi-coding-agent --with "qi-todo"
123
128
  ⚠ 无法解析的声明: git+https://host/repo
124
129
  用 `名字 @ URL` 写法才认得出名(如 qi-mcp @ git+https://host/repo)
125
- 已装但未声明(2): qi-mcp、qi-web —— 写进 settings.packages 才能在 uv tool 重建后补回
130
+ 已装但未声明(2): qi-mcp、qi-web —— pip 扩展只有声明了才会加载;
131
+ 写进 settings.packages 才生效(也是 uv tool 重建后能补回的依据)
132
+ qi-agents 被多级声明(user + project),以项目级为准
126
133
  ```
127
134
 
128
135
  四类信息各自的含义:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "qi-agents"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "qi 的角色系统扩展:角色发现 + 角色选择 + subagent 工具(进程内受管子运行)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "qi-mcp"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "qi 的 MCP 支持:两层 mcp.json 声明表 + 一个 mcp 代理工具 + lazy server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "qi-web"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "qi 的 Web UI 扩展:HTTP 宿主(REST + SSE)+ AG-UI 桥 + 官方界面"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "qi-coding-agent"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "编码 agent 框架:单 agent core + 扩展(MCP / 多 agent / web)"
9
9
  keywords = ["agent", "coding-agent", "cli", "tui", "llm", "mcp"]
10
10
  readme = "README.md"
@@ -15,4 +15,4 @@ import os
15
15
  # 开关语义见 litellm/litellm_core_utils/get_model_cost_map.py。
16
16
  os.environ.setdefault("LITELLM_LOCAL_MODEL_COST_MAP", "True")
17
17
 
18
- __version__ = "0.2.0"
18
+ __version__ = "0.2.1"
@@ -118,15 +118,37 @@ class ResolvedKey:
118
118
  key: str | None # None = 未找到
119
119
  source: str
120
120
  ok: bool
121
+ #: 密钥**明显不像密钥**的原因(`key_problem()` 给的),否则 None。
122
+ #: 保留 `ok=True`(密钥来源存在、provider 仍可被选中),但请求前会据此报清楚。
123
+ problem: str | None = None
121
124
 
122
125
  def describe(self) -> str:
123
126
  if self.source == "none":
124
127
  return "无需密钥(本地 provider)"
128
+ if self.problem:
129
+ return f"无效:{self.problem}({self.source})"
125
130
  if self.ok:
126
131
  return f"{self.source}({self.key[:6]}…)" if self.key else self.source
127
132
  return f"缺失({self.source})"
128
133
 
129
134
 
135
+ def key_problem(key: str) -> str | None:
136
+ """显然不能当 API key 的值 → 一句人话(否则 None)。
137
+
138
+ HTTP 头只能装 ASCII,所以含非 ASCII 的密钥一定会在请求时炸成
139
+ `'ascii' codec can't encode …: InternalServerError` —— 而那时离“输入”已经很远了。
140
+ 空白/换行同理(真密钥里不会有空格)。最常见的来源是**误粘贴**:把一段提示/日志
141
+ 粘进了密钥输入框。所以宁在入口就拦下。
142
+ """
143
+ if not key or not key.strip():
144
+ return "是空的"
145
+ if any(ord(ch) > 127 for ch in key):
146
+ return "含非 ASCII 字符(多半是把别的内容粘贴进来了)"
147
+ if any(ch.isspace() for ch in key):
148
+ return "含空白字符"
149
+ return None
150
+
151
+
130
152
  class AuthStore:
131
153
  """`~/.qi/agent/auth.json`:按 provider 存 {type:api_key, key:…},权限 0600。"""
132
154
 
@@ -155,6 +177,14 @@ class AuthStore:
155
177
  os.chmod(self.path, 0o600)
156
178
 
157
179
  def set_key(self, provider: str, key: str) -> None:
180
+ """存一个 API key。**显然不是密钥的值直接拒绝**(`ValueError`)。
181
+
182
+ 在入口拦(而不是等到请求时):非 ASCII / 带空格的密钥不可能是真的,而它一旦落盘,
183
+ 失败会拖到某个回合才以 `InternalServerError: 'ascii' codec …` 的形式暴露。
184
+ """
185
+ problem = key_problem(key)
186
+ if problem:
187
+ raise ValueError(f"{provider} 的 API key {problem}")
158
188
  data = self.load()
159
189
  data[provider] = {"type": "api_key", "key": key}
160
190
  self.save(data)
@@ -196,18 +226,24 @@ def resolve_key(provider: str, api_key_ref: str | None = None,
196
226
  1. auth store → 2. 约定环境变量 → 3. provider apiKey 引用。
197
227
  """
198
228
  store = store or AuthStore()
229
+
230
+ def _found(key: str, source: str) -> ResolvedKey:
231
+ # 来源存在 → `ok=True`(provider 仍可被选中);但“不像密钥”要在 `problem` 里留下,
232
+ # 让请求前能报“你的 key 有问题”,而不是拖到 HTTP 头编码失败。
233
+ return ResolvedKey(key=key, source=source, ok=True, problem=key_problem(key))
234
+
199
235
  stored = store.get(provider)
200
236
  if stored:
201
- return ResolvedKey(key=stored, source="auth", ok=True)
237
+ return _found(stored, "auth")
202
238
  default_env = DEFAULT_API_KEY_ENV.get(provider)
203
239
  if default_env:
204
240
  value = os.environ.get(default_env)
205
241
  if value:
206
- return ResolvedKey(key=value, source=f"env:{default_env}", ok=True)
242
+ return _found(value, f"env:{default_env}")
207
243
  if api_key_ref:
208
244
  value = resolve_value(api_key_ref)
209
245
  if value:
210
- return ResolvedKey(key=value, source=f"config:{api_key_ref}", ok=True)
246
+ return _found(value, f"config:{api_key_ref}")
211
247
  return ResolvedKey(key=None, source=f"config:{api_key_ref}(未解析)", ok=False)
212
248
  if default_env:
213
249
  return ResolvedKey(key=None, source=f"env:{default_env}(未设置)", ok=False)
@@ -842,14 +842,19 @@ def list_extensions() -> None:
842
842
  console.print("[dim]没有已装扩展,settings.packages 里也没有声明。[/dim]")
843
843
  console.print(f"[dim]装法:uv tool install {HOST_DISTRIBUTION} --with qi-mcp[/dim]")
844
844
  return
845
- declared_names = {item.name for item in report.declared}
845
+ declared_by = {item.name: item for item in report.declared}
846
846
  table = Table(title="扩展")
847
847
  table.add_column("扩展"); table.add_column("通道"); table.add_column("版本")
848
848
  table.add_column("来源"); table.add_column("声明")
849
849
  for item in report.installed:
850
+ decl = declared_by.get(item.name)
851
+ if decl is None:
852
+ mark = "否"
853
+ else:
854
+ # 声明**写在哪一级**(user = `~/.qi/agent/settings.json`;project = `<git根>/.qi/settings.json`)
855
+ mark = f"是({' + '.join(decl.scopes)})" if decl.scopes else "是"
850
856
  table.add_row(item.name, item.channel, item.version or "—",
851
- f"{item.origin} · {item.scope}",
852
- "是" if item.name in declared_names else "否")
857
+ f"{item.origin} · {item.scope}", mark)
853
858
  for decl in report.missing: # 声明了却加载不到 —— 单独一行,不混进已装列表
854
859
  table.add_row(decl.name, decl.channel, "—", f"{decl.spec} · {decl.source}", "未安装")
855
860
  console.print(table)
@@ -1388,7 +1393,11 @@ def auth_login(provider: str = typer.Argument(...)) -> None:
1388
1393
  provider = provider.strip().lower()
1389
1394
  key = prompt.text(f"{provider} API key", required=True)
1390
1395
  store = AuthStore()
1391
- store.set_key(provider, key)
1396
+ try:
1397
+ store.set_key(provider, key)
1398
+ except ValueError as exc: # 显然不是密钥(误粘贴)→ 入口就拦
1399
+ err_console.print(f"[red]{escape(str(exc))}[/red]")
1400
+ raise typer.Exit(code=2) from exc
1392
1401
  console.print(f"[green]已保存 {provider} 到 {store.path}(0600): [/green]{escape(_mask(key))}")
1393
1402
 
1394
1403
 
@@ -1657,8 +1666,13 @@ def _configure_provider(provider: str, entry: dict) -> None:
1657
1666
  key = prompt.text(f"{provider} API key", suffix=suffix,
1658
1667
  required=not current_key and not env_name)
1659
1668
  if key:
1660
- store.set_key(provider, key)
1661
- current_key = key
1669
+ try:
1670
+ store.set_key(provider, key)
1671
+ except ValueError as exc: # 显然不是密钥(误粘贴)
1672
+ console.print(f"[red]{escape(str(exc))}[/red]")
1673
+ key = ""
1674
+ else:
1675
+ current_key = key
1662
1676
  shown = _mask(current_key) if current_key else (
1663
1677
  f"${env_name}(环境变量)" if env_name else "(未设置)")
1664
1678
  summary = f"[green]✓[/green] {provider} — API Key: {escape(shown)}"
@@ -1860,7 +1874,11 @@ def _init_noninteractive(*, provider: str | None, model: str | None, base_url: s
1860
1874
 
1861
1875
  store = AuthStore()
1862
1876
  if api_key:
1863
- store.set_key(provider, api_key)
1877
+ try:
1878
+ store.set_key(provider, api_key)
1879
+ except ValueError as exc: # 显然不是密钥(误粘贴)
1880
+ err_console.print(f"[red]{escape(str(exc))}[/red]")
1881
+ raise typer.Exit(code=2) from exc
1864
1882
  elif api_key_env:
1865
1883
  entry["apiKey"] = f"${api_key_env}"
1866
1884
  elif not entry.get("apiKey") and not store.get(provider):
@@ -315,6 +315,30 @@ def _as_int(value: Any, default: int = 0) -> int:
315
315
  return default
316
316
 
317
317
 
318
+ class ProviderAuthError(RuntimeError):
319
+ """凭证侧可解释的失败(密钥不像密钥 / 请求头装不下)。
320
+
321
+ 存在的理由:底层 litellm 把这种错误包成
322
+ `InternalServerError: … 'ascii' codec can't encode …`,**看不出是 API key 的问题**。
323
+ 在请求前/请求时把它翻成一句直接指向凭证的话。
324
+ """
325
+
326
+
327
+ def friendly_provider_error(exc: Exception) -> Exception:
328
+ """把 provider 侧那句只有工程师看得懂的底层错误,翻成带下一步动作的。
329
+
330
+ 命中 `'ascii'/'latin-1' codec can't encode`:HTTP 头/URL 装不下这个值 —— 最常见的就是
331
+ API key 里混入了非 ASCII 字符(把一段中文提示误粘贴进了密钥框)。
332
+ """
333
+ text = str(exc)
334
+ if "'ascii' codec can't encode" in text or "'latin-1' codec can't encode" in text:
335
+ return ProviderAuthError(
336
+ "请求头里有非 ASCII 字符(HTTP 头只能是 ASCII)—— 最常见的是 **API key 里混入了"
337
+ "中文/全角字符**(比如误把一段提示粘进了密钥框)。检查 `qi auth list`、对应的"
338
+ "环境变量(如 `DEEPSEEK_API_KEY`)、以及 `models.json` 里的 `apiKey` 来源。")
339
+ return exc
340
+
341
+
318
342
  class LiteLLMClient:
319
343
  """基于 litellm 的实现。spec 来自 resolve_model()(经 resolve_key 取 key)。"""
320
344
 
@@ -389,6 +413,13 @@ class LiteLLMClient:
389
413
  def _base_kwargs(self, messages: list[ChatMessage], tools: list[dict] | None,
390
414
  temperature: float | None) -> dict:
391
415
  """chat 与 astream 共用的请求参数(避免两处漂移)。"""
416
+ if self._resolved.problem:
417
+ # 在发请求**之前**就报 —— 否则要拖到 litellm 拼 `Authorization: Bearer …`
418
+ # 时以 `'ascii' codec can't encode` 暴露,看不出是 key 的问题。
419
+ raise ProviderAuthError(
420
+ f"{self.spec.provider} 的 API key {self._resolved.problem}"
421
+ f"(来源:{self._resolved.source})。HTTP 头只能是 ASCII,这个值发不出去 —— "
422
+ f"重新登录(如 `qi auth login {self.spec.provider}`)或修正该来源。")
392
423
  kwargs: dict = {
393
424
  "model": self.model_name,
394
425
  "messages": [m.to_dict() for m in messages],
@@ -467,7 +498,7 @@ class LiteLLMClient:
467
498
  resp = await litellm.acompletion(**kwargs)
468
499
  except Exception as exc:
469
500
  if not self._consider_reasoning_rejection(exc):
470
- raise
501
+ raise friendly_provider_error(exc) from exc
471
502
  # 去掉 reasoning_effort 重试一次(用户只想调级别,不该因此整轮失败)
472
503
  kwargs = await self._prepare_headers(
473
504
  await self._prepare_request(self._base_kwargs(messages, tools, temperature)))
@@ -522,6 +553,9 @@ class LiteLLMClient:
522
553
  try:
523
554
  return await self._open_stream(messages, tools, temperature, with_usage)
524
555
  except Exception as exc: # noqa: BLE001 打开流失败:逐项降级
556
+ friendly = friendly_provider_error(exc)
557
+ if isinstance(friendly, ProviderAuthError):
558
+ raise friendly from exc # 凭证问题不该被当成“参数不被接受”吞掉
525
559
  if self._consider_reasoning_rejection(exc):
526
560
  continue
527
561
  if with_usage:
@@ -582,10 +616,10 @@ class LiteLLMClient:
582
616
  yield LLMDelta(text=text)
583
617
  for tc in getattr(delta_obj, "tool_calls", None) or []:
584
618
  merge_tool_call_delta(merged, tc)
585
- except Exception:
619
+ except Exception as exc:
586
620
  # 首片就失败 → 可安全降级;已吐过字/思考 → 不能重试(会重复输出)
587
621
  if content_seen:
588
- raise
622
+ raise friendly_provider_error(exc) from exc
589
623
  self._stream_broken = True
590
624
  async for delta in chat_as_stream(self, messages, tools, temperature):
591
625
  yield delta
@@ -60,7 +60,7 @@ from .session import (MODEL_CHANGE, THINKING_LEVEL_CHANGE, Session, SessionStore
60
60
  from .titling import suggest_title
61
61
  from .settings import (default_tools, extension_dirs, load_settings, load_settings_by_scope,
62
62
  branch_summary_skip_prompt, model_thinking_level,
63
- resolve_project_trust, session_dir)
63
+ project_declared_default_trust, resolve_project_trust, session_dir)
64
64
  from .tools import ToolContext, register_builtin_tools
65
65
  from .tools.shell import session_env
66
66
 
@@ -282,8 +282,7 @@ class QiRuntime:
282
282
  #: 与 `_level_pins` 分开:那是会话级的,这个是**整场运行**的。
283
283
  #: 命令行给的值是当次显式覆盖,不该被会话里记的旧值顶掉(与 `_model_pinned` 对称)。
284
284
  self._level_pinned_cli = False
285
- declared_by_repo = (getattr(scopes.get("project"), "defaultProjectTrust", None)
286
- or "").strip()
285
+ declared_by_repo = project_declared_default_trust(self.cwd) or ""
287
286
  if declared_by_repo:
288
287
  # 不静默:一个仓库自称可信是值得报警的事
289
288
  self.notes.append(
@@ -410,6 +410,22 @@ def project_trust_default(settings: QiSettings) -> str:
410
410
  return value if value in ("ask", "always", "never") else "ask"
411
411
 
412
412
 
413
+ def project_declared_default_trust(cwd: Path | None = None) -> str | None:
414
+ """项目级 `settings.json` **原始文件**里显式写的 `defaultProjectTrust`(没写 → None)。
415
+
416
+ 为什么不能直接读解析后的 `QiSettings`:那个字段默认值就是 `"ask"`,`getattr` 分不出
417
+ “没写”与“写了 ask” —— 会把**任何**有 `.qi/settings.json` 的仓库都误报成“仓库自称可信”。
418
+ """
419
+ for path, scope in settings_file_candidates(cwd):
420
+ if scope != "project":
421
+ continue
422
+ if not path.is_file():
423
+ return None
424
+ value = read_json(path).get("defaultProjectTrust")
425
+ return str(value).strip() if value is not None else None
426
+ return None
427
+
428
+
413
429
  def resolve_project_trust(settings: QiSettings | None, *,
414
430
  approve: bool | None = None,
415
431
  has_ui: bool = False,
@@ -3298,7 +3298,10 @@ class QiTui(App):
3298
3298
  return self._editor_component_factory
3299
3299
 
3300
3300
  def _append(self, widget) -> None:
3301
- log = self.query_one("#log", VerticalScroll)
3301
+ try:
3302
+ log = self.query_one("#log", VerticalScroll)
3303
+ except (NoMatches, ScreenStackError):
3304
+ return # 卸载竞态:worker 在 teardown 之后才报错,没地方可写
3302
3305
  if len(log.children):
3303
3306
  log.mount(Blank())
3304
3307
  log.mount(widget)
@@ -4726,6 +4729,9 @@ class QiTui(App):
4726
4729
  except OSError as exc: # 读/写盘失败(权限、只读 home…)
4727
4730
  self._note(f"写凭证失败: {exc}", "error")
4728
4731
  return
4732
+ except ValueError as exc: # 显然不是密钥(误粘贴)→ 入口就拦
4733
+ self._note(f"没保存:{exc}。重新 `/login` 粘贴正确的 API key。", "error")
4734
+ return
4729
4735
  self._note(f"已保存 {provider} 的 API key → {store.path}(0600)", "info")
4730
4736
  if provider in self._preset_providers():
4731
4737
  self._note(f"{provider} 来自**预置**(models.json 里没写,qi 的内置表兜底);"
@@ -0,0 +1,96 @@
1
+ """API key 入口校验:显然不是密钥的值在**写入时**就拒绝。
2
+
3
+ 真实事故:把一段中文提示误粘贴进了 `/login` 的密钥框、落进 `auth.json`;之后某个回合
4
+ 才以 `InternalServerError: 'ascii' codec can't encode characters in position 7-13`
5
+ 暴露 —— 离“输入”已经很远(litellm 拼出 `Authorization: Bearer 检测到角色目录…`)。
6
+ 所以校验放在 `AuthStore.set_key`,而不是等请求时。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
15
+
16
+ import pytest # noqa: E402
17
+
18
+ from qi_agent.auth import AuthStore, key_problem # noqa: E402
19
+
20
+
21
+ def test_key_problem_accepts_real_looking_keys():
22
+ assert key_problem("sk-abc123-XYZ_09.+/=") is None
23
+ assert key_problem("ghp_" + "a" * 40) is None
24
+
25
+
26
+ @pytest.mark.parametrize("bad", [
27
+ "", " ",
28
+ "sk-a b", # 空格
29
+ "sk-a\nb", # 换行
30
+ "检测到角色目录(/x)但没有任何扩展读取它", # 误粘贴的中文提示
31
+ "sk-键", # 单个非 ASCII 字符
32
+ ])
33
+ def test_key_problem_rejects_obvious_non_keys(bad):
34
+ assert key_problem(bad)
35
+
36
+
37
+ def test_set_key_refuses_a_pasted_warning(tmp_path):
38
+ store = AuthStore(tmp_path / "auth.json")
39
+ with pytest.raises(ValueError):
40
+ store.set_key("deepseek", "检测到角色目录(/x)但没有任何扩展读取它")
41
+ assert not (tmp_path / "auth.json").exists(), "被拒的值不该落盘"
42
+
43
+
44
+ def test_set_key_still_stores_a_real_looking_key(tmp_path):
45
+ store = AuthStore(tmp_path / "auth.json")
46
+ store.set_key("deepseek", "sk-real-key")
47
+ assert store.get("deepseek") == "sk-real-key"
48
+
49
+
50
+ def test_resolve_key_flags_an_already_stored_bad_key(tmp_path):
51
+ """旧版本/手工编辑落下的坏值:解析时不隐藏 provider,但把问题标出来。"""
52
+ from qi_agent.auth import resolve_key
53
+
54
+ store = AuthStore(tmp_path / "auth.json")
55
+ store.save({"deepseek": {"type": "api_key", "key": "sk-中文"}})
56
+ rk = resolve_key("deepseek", store=store)
57
+ assert rk.ok and rk.key == "sk-中文" # 来源还在 → 仍可被选中/诊断
58
+ assert rk.problem and "非 ASCII" in rk.problem
59
+ assert "无效" in rk.describe()
60
+
61
+
62
+ @pytest.mark.asyncio
63
+ async def test_client_refuses_a_bad_key_before_the_request(tmp_path):
64
+ """请求**之前**就报“你的 key 有问题”,而不是等 provider 侧的 ascii 错误。"""
65
+ from qi_agent.config import ResolvedModel
66
+ from qi_agent.llm import ChatMessage, LiteLLMClient, ProviderAuthError
67
+
68
+ store = AuthStore(tmp_path / "auth.json")
69
+ store.save({"deepseek": {"type": "api_key", "key": "检测到角色目录(误粘贴)"}})
70
+ spec = ResolvedModel(provider="deepseek", model="deepseek-chat", api="openai-completions",
71
+ base_url=None, api_key_ref=None, reasoning=False,
72
+ context_window=8192, max_tokens=1024)
73
+ client = LiteLLMClient(spec, store)
74
+ with pytest.raises(ProviderAuthError) as excinfo:
75
+ await client.chat([ChatMessage(role="user", content="hi")])
76
+ msg = str(excinfo.value)
77
+ assert "API key" in msg and "deepseek" in msg and "auth" in msg
78
+
79
+
80
+ def test_friendly_provider_error_translates_the_ascii_codec_failure():
81
+ """底层那句 `'ascii' codec …` 要翻成直接指向凭证的话。"""
82
+ from qi_agent.llm import ProviderAuthError, friendly_provider_error
83
+
84
+ raw = RuntimeError(
85
+ "litellm.InternalServerError: InternalServerError: OpenAIException - "
86
+ "'ascii' codec can't encode characters in position 7-13: ordinal not in range(128)")
87
+ friendly = friendly_provider_error(raw)
88
+ assert isinstance(friendly, ProviderAuthError)
89
+ assert "API key" in str(friendly)
90
+
91
+
92
+ def test_friendly_provider_error_leaves_other_errors_alone():
93
+ from qi_agent.llm import friendly_provider_error
94
+
95
+ raw = RuntimeError("connection reset")
96
+ assert friendly_provider_error(raw) is raw
@@ -444,3 +444,36 @@ def test_installer_cmd_strips_yes_for_uv_uninstall(tmp_path, monkeypatch):
444
444
  cmd = cli_mod._installer_cmd(["uninstall", "-y", "qi-mcp"])
445
445
  assert cmd == ["/usr/bin/uv", "pip", "uninstall", "--python",
446
446
  str(root / "bin" / "python"), "qi-mcp"]
447
+
448
+
449
+ # ── qi list:声明写在哪一级 ────────────────────────────────────────────
450
+
451
+
452
+ def test_list_shows_the_declaring_scope(tmp_path, monkeypatch):
453
+ """`声明` 列要能看出用户级还是项目级 —— 否则和「来源·env」混在一起看不出写在哪。"""
454
+ from qi_agent.packages import DeclaredPackage, InstalledExtension, PackageReport
455
+
456
+ report = PackageReport(
457
+ installed=[InstalledExtension("qi-mcp", "pip", "qi-mcp 0.1.1", "env")],
458
+ declared=[DeclaredPackage("qi_mcp-0.1.1.tar.gz", "qi-mcp", "pip",
459
+ "settings:project", requirement="qi_mcp-0.1.1.tar.gz",
460
+ scopes=("project",))],
461
+ )
462
+ monkeypatch.setattr(cli_mod, "package_report", lambda cwd=None: report)
463
+ res = runner.invoke(app, ["list"])
464
+ assert res.exit_code == 0, res.output
465
+ assert "是(project)" in res.output
466
+
467
+
468
+ def test_list_shows_both_scopes_when_declared_twice(tmp_path, monkeypatch):
469
+ from qi_agent.packages import DeclaredPackage, InstalledExtension, PackageReport
470
+
471
+ report = PackageReport(
472
+ installed=[InstalledExtension("qi-mcp", "pip", "qi-mcp 0.1.1", "env")],
473
+ declared=[DeclaredPackage("qi-mcp", "qi-mcp", "pip", "settings:project",
474
+ scopes=("user", "project"))],
475
+ )
476
+ monkeypatch.setattr(cli_mod, "package_report", lambda cwd=None: report)
477
+ res = runner.invoke(app, ["list"])
478
+ assert res.exit_code == 0, res.output
479
+ assert "是(user + project)" in res.output
@@ -70,6 +70,17 @@ def test_repo_cannot_declare_itself_trusted(tmp_path, monkeypatch):
70
70
  assert any("defaultProjectTrust" in n and "已忽略" in n for n in rt.notes), rt.notes
71
71
 
72
72
 
73
+ def test_project_settings_without_the_key_does_not_warn(tmp_path, monkeypatch):
74
+ """回归:`QiSettings.defaultProjectTrust` 的默认值就是 `"ask"`,不能用 getattr 判“写没写”。
75
+
76
+ 否则**任何**有 `.qi/settings.json` 的仓库(哪怕只是写了 `packages`)都会被误报成
77
+ “项目 settings 里写了 defaultProjectTrust=ask”。
78
+ """
79
+ project = _env(tmp_path, monkeypatch, user={}, project={"packages": ["qi-mcp"]})
80
+ rt = _runtime(project)
81
+ assert not any("已忽略" in n for n in rt.notes), rt.notes
82
+
83
+
73
84
  def test_user_level_always_still_trusts(tmp_path, monkeypatch):
74
85
  """正当路径不能被误伤:用户在**自己家里**表态就生效。"""
75
86
  project = _env(tmp_path, monkeypatch, user={"defaultProjectTrust": "always"})
@@ -160,3 +160,28 @@ def test_headless_runtime_still_requires_a_default_model(tmp_path, monkeypatch):
160
160
  project = _env(tmp_path, monkeypatch)
161
161
  with pytest.raises(ConfigError):
162
162
  QiRuntime(cwd=project) # has_ui 默认 False
163
+
164
+
165
+ @pytest.mark.asyncio
166
+ async def test_login_refuses_a_pasted_warning_instead_of_a_key(tmp_path, monkeypatch):
167
+ """误把一段中文提示粘进密钥框:入口就拒绝,不落盘、不选中模型。
168
+
169
+ 落盘的后果是**很晚**才以 `InternalServerError: 'ascii' codec …` 暴露。
170
+ """
171
+ _env(tmp_path, monkeypatch)
172
+
173
+ app = tui_mod.QiTui(palette=PALETTE, approve_project=True)
174
+
175
+ async def fake_screen(screen):
176
+ return "检测到角色目录(/x)但没有任何扩展读取它 —— 这些角色不会被使用"
177
+
178
+ async with app.run_test(size=(100, 30)) as pilot:
179
+ await pilot.pause(0.3)
180
+ app.await_screen = fake_screen # type: ignore[method-assign]
181
+ app._command("/login deepseek")
182
+ await app.workers.wait_for_complete()
183
+ await pilot.pause(0.2)
184
+
185
+ assert app._model is None, "拒绝之后不该选中任何模型"
186
+ assert "没保存" in _log_text(app)
187
+ assert not (tmp_path / "home" / "auth.json").exists(), "被拒的值不该落盘"
File without changes