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.
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/PKG-INFO +2 -1
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/README.md +1 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/models.md +1 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/packages.md +28 -21
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/pyproject.toml +1 -1
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/pyproject.toml +1 -1
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/pyproject.toml +1 -1
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/pyproject.toml +1 -1
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/__init__.py +1 -1
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/auth.py +39 -3
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/cli.py +25 -7
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/llm.py +37 -3
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/runtime.py +2 -3
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/settings.py +16 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/tui.py +7 -1
- qi_coding_agent-0.2.1/tests/test_auth_key.py +96 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_install.py +33 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_trust_scope.py +11 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_no_model.py +25 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/.github/workflows/publish.yml +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/.gitignore +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/.pi-lens.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/LICENSE +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/THIRD_PARTY_NOTICES.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/PLAN.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/agent-config-design.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/bash-allowlist.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/development.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/dispatcher.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/extensions-design.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/internals.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/overview.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/pi-alignment.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/plugins.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/design/web.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/cli.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/compaction.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/configuration.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/containerization.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/docs.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/environment-variables.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/extensions.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/how-qi-works.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/index.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/json.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/keybindings.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/providers.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/quickstart.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/security.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/session-format.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/sessions.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/settings.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/skills.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/slash-commands.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/themes.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/tui.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/docs/usage.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/README.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-analyst/agent.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-analyst/assets/review-rules.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-analyst/mcp.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-analyst/skills/code-review-checklist/SKILL.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/code-reviewer/agent.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/data-analyst/agent.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/doc-writer/agent.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/doc-writer/assets/doc-style.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/general/agent.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/test-runner/agent.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/agents/writer/agent.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/extensions/README.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/extensions/hello/README.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/examples/extensions/hello/extension.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/README.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/qi_agents/__init__.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/qi_agents/discovery.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-agents/qi_agents/subagent.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/README.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/__init__.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/client.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/config.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/direct.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/proxy.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/role.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-mcp/qi_mcp/servers.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/README.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/__init__.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/agui.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/app.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/browse.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/files.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/schemas.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/security.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/serve.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/state.py +0 -0
- {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
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/static/assets/index-DePFgopp.css +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/qi_web/static/index.html +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/index.html +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/package-lock.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/package.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/scripts/check-design.mjs +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/scripts/check-dsh-tokens.mjs +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/App.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/api/client.test.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/api/client.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/api/contract.test.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/api/types.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/commands.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/AgentMenu.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/CommandMenu.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/ConfirmDialog.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/DirPicker.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/Dock.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/FilePanel.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/ModelMenu.test.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/ModelMenu.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/ProjectMenu.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/Rail.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/RowMenu.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/Rows.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/Settings.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/SidebarSection.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/components/icons.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/main.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/markdown/cjk.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/markdown/markdown.test.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/markdown/parse.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/markdown/render.tsx +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/projects.test.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/projects.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/route.test.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/route.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/stats.test.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/stats.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/turn.test.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/state/turn.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/theme/app.css +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/theme/theme.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/theme/tokens.css +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/src/vite-env.d.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/tsconfig.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/vite.config.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/extensions/qi-web/ui/vitest.config.ts +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/pyrightconfig.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/scripts/check_versions.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/scripts/e2e_ui_model.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/scripts/prune_empty_sessions.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/abort.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/compaction.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/config.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/export_html.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/extensions.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/loader.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/model_catalog.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/models.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/packages.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/paths.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/pep723.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/presets.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/prompt.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/registry.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/runner.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/session.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/share.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/system_prompt.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/theme.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/themes/README.md +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/themes/dark.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/themes/light.json +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/titling.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/tools/__init__.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/tools/shell.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/trust.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/src/qi_agent/workspaces.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/conftest.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_branch_summary_prompt.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_browse.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_init.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_pi_alignment.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_run.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_cli_tool_flags.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_compaction.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_compaction_events.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_config.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_config_resources.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_contract_p0.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_docs_injection.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_doctor_extensions.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_example_extension.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_export_html.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_alignment.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_bus.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_cli_commands.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_commands.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_deps.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_entry_points.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_events.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_exclusions.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_flags.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_interop.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_messages.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_model.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_resolvers.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_run_agent.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_runner_events.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_session.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_tools.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extension_ui.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_extensions.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_interrupt.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_litellm_offline.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_missing_extension_hints.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_model_catalog.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_model_thinking_levels.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_package_resource_toggle.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_packages.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_pep723.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_presets.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_prune_empty_sessions.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_agents_extension.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_config.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_direct.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_http.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_proxy.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_role.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_qi_mcp_stdio.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_runner_core.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_session_model_entries.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_session_store.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_session_tree.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_settings_skills.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_share.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_shell.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_skill_commands.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_streaming.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_system_prompt.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_theme.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_thinking.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_thinking_budgets.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_third_party_notices.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_titling.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tool_execution_mode.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_extension_render.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_model_restore.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_smoke.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_tui_style.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_web_api.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_web_extensions_api.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_web_files.py +0 -0
- {qi_coding_agent-0.2.0 → qi_coding_agent-0.2.1}/tests/test_workspaces.py +0 -0
- {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.
|
|
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
|
然后直接说你要做什么。三种跑法:
|
|
@@ -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
|
-
装好了但没声明(这份环境里三个扩展都是
|
|
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 ·
|
|
100
|
-
│ qi-agents │ pip │ 0.1.0 │ qi-agents 0.1.0 ·
|
|
101
|
-
│ qi-web │ pip │ 0.1.0 │ qi-web 0.1.0 ·
|
|
102
|
-
|
|
103
|
-
已装但未声明(3): qi-mcp、qi-agents、qi-web ——
|
|
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
|
-
|
|
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 ·
|
|
115
|
-
│ qi-agents │ pip │ 0.1.0 │ qi-agents 0.1.0 ·
|
|
116
|
-
│ qi-web │ pip │ 0.1.0 │ qi-web 0.1.0 ·
|
|
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 ——
|
|
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-coding-agent"
|
|
7
|
-
version = "0.2.
|
|
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"
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
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
|
-
|
|
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 = (
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|