kimi-code-usage 0.1.4__tar.gz → 0.1.5__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 (110) hide show
  1. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/PKG-INFO +1 -1
  2. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/pyproject.toml +1 -1
  3. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/main.py +175 -5
  4. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/providers/kimi.py +19 -4
  5. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/test_main.py +66 -0
  6. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/test_provider_kimi.py +40 -0
  7. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/uv.lock +1 -1
  8. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/.claude/settings.local.json +0 -0
  9. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/.env.example +0 -0
  10. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/.github/workflows/ci.yml +0 -0
  11. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/.gitignore +0 -0
  12. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/AGENTS.md +0 -0
  13. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/README.md +0 -0
  14. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/README.zh.md +0 -0
  15. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/kimi-code-usage.md +0 -0
  16. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/plans/2026-05-09-pace-indicator.md +0 -0
  17. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/plans/2026-05-14-pace-theme-thresholds.md +0 -0
  18. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/plans/2026-06-10-multi-llm-quota.md +0 -0
  19. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/plans/2026-06-10-vscode-extension-ts-strictness.md +0 -0
  20. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/specs/2026-05-09-pace-indicator-design.md +0 -0
  21. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/specs/2026-05-14-pace-theme-thresholds-design.md +0 -0
  22. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/specs/2026-06-10-multi-llm-quota-design.md +0 -0
  23. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/specs/2026-06-10-vscode-extension-multi-provider-design.md +0 -0
  24. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/docs/superpowers/specs/2026-06-10-vscode-extension-ts-refactor-design.md +0 -0
  25. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/.gitignore +0 -0
  26. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/class_index.html +0 -0
  27. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
  28. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
  29. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/function_index.html +0 -0
  30. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/index.html +0 -0
  31. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
  32. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/status.json +0 -0
  33. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/style_cb_4667309f.css +0 -0
  34. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_581a990e2fe7e610___init___py.html +0 -0
  35. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_581a990e2fe7e610_config_py.html +0 -0
  36. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_581a990e2fe7e610_main_py.html +0 -0
  37. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_581a990e2fe7e610_mcp_py.html +0 -0
  38. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_aaf3485d2243f1af___init___py.html +0 -0
  39. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_aaf3485d2243f1af_anthropic_py.html +0 -0
  40. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_aaf3485d2243f1af_kimi_py.html +0 -0
  41. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_aaf3485d2243f1af_openai_py.html +0 -0
  42. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/htmlcov/z_aaf3485d2243f1af_openrouter_py.html +0 -0
  43. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/kimi_usage.code-workspace +0 -0
  44. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/requirements.txt +0 -0
  45. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/__init__.py +0 -0
  46. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/config.py +0 -0
  47. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/mcp.py +0 -0
  48. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/providers/__init__.py +0 -0
  49. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/providers/anthropic.py +0 -0
  50. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/providers/openai.py +0 -0
  51. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/src/kimi_code_usage/providers/openrouter.py +0 -0
  52. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/__init__.py +0 -0
  53. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/conftest.py +0 -0
  54. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/test_config.py +0 -0
  55. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/test_mcp.py +0 -0
  56. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/test_provider_anthropic.py +0 -0
  57. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/test_provider_openai.py +0 -0
  58. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/test_provider_openrouter.py +0 -0
  59. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/tests/test_providers_init.py +0 -0
  60. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/.claude/settings.local.json +0 -0
  61. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/.github/workflows/ci.yml +0 -0
  62. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/.prettierignore +0 -0
  63. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/.prettierrc.json +0 -0
  64. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/.vscodeignore +0 -0
  65. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/LICENSE +0 -0
  66. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/README.md +0 -0
  67. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/assets/icon.png +0 -0
  68. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/assets/moon.svg +0 -0
  69. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/eslint.config.mjs +0 -0
  70. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.de.json +0 -0
  71. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.en.json +0 -0
  72. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.es.json +0 -0
  73. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.fr.json +0 -0
  74. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.it.json +0 -0
  75. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.ja.json +0 -0
  76. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.ko.json +0 -0
  77. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.pt.json +0 -0
  78. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.ru.json +0 -0
  79. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/l10n/bundle.l10n.zh-cn.json +0 -0
  80. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/package.json +0 -0
  81. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/scripts/copy-assets.ts +0 -0
  82. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/__mocks__/vscode.ts +0 -0
  83. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/api.test.ts +0 -0
  84. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/api.ts +0 -0
  85. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/apiCache.test.ts +0 -0
  86. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/apiCache.ts +0 -0
  87. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/config.test.ts +0 -0
  88. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/config.ts +0 -0
  89. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/extension.test.ts +0 -0
  90. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/extension.ts +0 -0
  91. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/historyPanel.test.ts +0 -0
  92. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/historyPanel.ts +0 -0
  93. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/i18n.test.ts +0 -0
  94. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/i18n.ts +0 -0
  95. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/notifier.test.ts +0 -0
  96. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/notifier.ts +0 -0
  97. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/pace.test.ts +0 -0
  98. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/pace.ts +0 -0
  99. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/predict.test.ts +0 -0
  100. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/predict.ts +0 -0
  101. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/statusBar.test.ts +0 -0
  102. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/statusBar.ts +0 -0
  103. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/storage-unlink.test.ts +0 -0
  104. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/storage.test.ts +0 -0
  105. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/storage.ts +0 -0
  106. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/types.ts +0 -0
  107. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/src/vscode-mock.test.ts +0 -0
  108. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/tsconfig.eslint.json +0 -0
  109. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/tsconfig.json +0 -0
  110. {kimi_code_usage-0.1.4 → kimi_code_usage-0.1.5}/vscode-extension/vitest.config.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kimi-code-usage
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A curated Kimi Coding Plan usage monitor with aesthetic CLI and MCP server.
5
5
  Project-URL: Homepage, https://github.com/Golden0Voyager/kimi-code-usage
6
6
  Project-URL: Repository, https://github.com/Golden0Voyager/kimi-code-usage
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kimi-code-usage"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "A curated Kimi Coding Plan usage monitor with aesthetic CLI and MCP server."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -11,7 +11,7 @@ from rich.live import Live
11
11
  from rich.panel import Panel
12
12
  from rich.text import Text
13
13
 
14
- from kimi_code_usage.config import AppConfig, ConfigResolver, save_theme
14
+ from kimi_code_usage.config import DEFAULT_CONFIG_PATH, AppConfig, ConfigResolver, save_theme
15
15
  from kimi_code_usage.providers import DailyUsage, ProviderUsage, dispatch_all
16
16
 
17
17
  # --- i18n ---
@@ -42,6 +42,41 @@ L_ZH = {
42
42
 
43
43
  L = L_ZH if IS_ZH else L_EN
44
44
 
45
+ _PROVIDER_SETUP_HELP = {
46
+ "kimi": {
47
+ "name": "Kimi Code",
48
+ "key_envs": ("KIMI_API_KEY", "KIMI_CODING_API_KEY"),
49
+ "base_env": "KIMI_BASE_URL",
50
+ "default_url": "https://api.kimi.com/coding/v1",
51
+ "note_en": "Use a Kimi Code / Coding Plan key (sk-kimi-...). Do not use https://api.moonshot.cn/v1.",
52
+ "note_zh": "使用 Kimi Code / Coding Plan 密钥(sk-kimi-...),不要使用 https://api.moonshot.cn/v1。",
53
+ },
54
+ "openai": {
55
+ "name": "OpenAI",
56
+ "key_envs": ("OPENAI_API_KEY",),
57
+ "base_env": "OPENAI_BASE_URL",
58
+ "default_url": "https://api.openai.com",
59
+ "note_en": "Use an OpenAI API key with organization usage access.",
60
+ "note_zh": "使用具备用量接口权限的 OpenAI API Key。",
61
+ },
62
+ "anthropic": {
63
+ "name": "Anthropic",
64
+ "key_envs": ("ANTHROPIC_API_KEY",),
65
+ "base_env": "ANTHROPIC_BASE_URL",
66
+ "default_url": "https://api.anthropic.com",
67
+ "note_en": "Use an Anthropic key that can access the usage endpoint.",
68
+ "note_zh": "使用具备用量接口权限的 Anthropic Key。",
69
+ },
70
+ "openrouter": {
71
+ "name": "OpenRouter",
72
+ "key_envs": ("OPENROUTER_API_KEY", "OPENROUTER_ADMIN_KEY"),
73
+ "base_env": "OPENROUTER_BASE_URL",
74
+ "default_url": "https://openrouter.ai/api",
75
+ "note_en": "Set OPENROUTER_MANAGEMENT_KEY or OPENROUTER_ADMIN_KEY for activity details.",
76
+ "note_zh": "如需活动明细,设置 OPENROUTER_MANAGEMENT_KEY 或 OPENROUTER_ADMIN_KEY。",
77
+ },
78
+ }
79
+
45
80
  def _get_visual_width(s: str) -> int:
46
81
  import unicodedata
47
82
  width = 0
@@ -52,6 +87,14 @@ def _get_visual_width(s: str) -> int:
52
87
  width += 1
53
88
  return width
54
89
 
90
+
91
+ def _mask_secret(secret: str | None) -> str:
92
+ if not secret:
93
+ return "missing"
94
+ if len(secret) <= 8:
95
+ return secret[:2] + "..."
96
+ return f"{secret[:7]}...{secret[-4:]}"
97
+
55
98
  def _get_localized_label(label: str, lang_zh: bool = IS_ZH) -> str:
56
99
  _L = L_ZH if lang_zh else L_EN
57
100
 
@@ -155,6 +198,117 @@ def _get_localized_text_value(text_val: str, lang_zh: bool) -> str:
155
198
 
156
199
  return text_val
157
200
 
201
+
202
+ def _render_interactive_help(lang_zh: bool, or_metric: str = "requests", days_window: int = 30) -> Text:
203
+ text = Text()
204
+ title = "交互帮助" if lang_zh else "Interactive Help"
205
+ text.append(f"{title}\n\n", style="bold")
206
+ if lang_zh:
207
+ rows = [
208
+ ("q / Ctrl-C", "退出"),
209
+ ("r", "刷新用量"),
210
+ ("h / ?", "显示或隐藏帮助"),
211
+ ("c", "显示或隐藏配置引导"),
212
+ ("1-4", "切换服务商面板"),
213
+ ("l", "切换中英文"),
214
+ ("←/→ 或 [ / ]", "切换主题"),
215
+ ("m", f"切换 OpenRouter 指标(当前:{_metric_label(or_metric, True)})"),
216
+ ("d", f"切换 OpenRouter 图表窗口(当前:{days_window}d)"),
217
+ ("Enter", "保存主题、语言、可见面板和 OpenRouter 显示偏好"),
218
+ ]
219
+ footer = "再次按 h/? 或 c 可回到用量视图。"
220
+ else:
221
+ rows = [
222
+ ("q / Ctrl-C", "quit"),
223
+ ("r", "refresh usage"),
224
+ ("h / ?", "show or hide help"),
225
+ ("c", "show or hide configuration guide"),
226
+ ("1-4", "toggle provider panels"),
227
+ ("l", "toggle language"),
228
+ ("←/→ or [ / ]", "cycle theme"),
229
+ ("m", f"cycle OpenRouter metric (current: {_metric_label(or_metric, False)})"),
230
+ ("d", f"cycle OpenRouter chart window (current: {days_window}d)"),
231
+ ("Enter", "save theme, language, visible panels, and OpenRouter display preferences"),
232
+ ]
233
+ footer = "Press h/? or c again to return to the usage view."
234
+
235
+ for key, desc in rows:
236
+ text.append(f" {key:<16}", style="bold")
237
+ text.append(f"{desc}\n", style="grey62")
238
+ text.append(f"\n{footer}", style="grey62")
239
+ return text
240
+
241
+
242
+ def _render_config_guide(
243
+ config: AppConfig,
244
+ lang_zh: bool = IS_ZH,
245
+ config_path: str | None = None,
246
+ ) -> Text:
247
+ text = Text()
248
+ title = "配置引导" if lang_zh else "Configuration Guide"
249
+ path = config_path or str(DEFAULT_CONFIG_PATH)
250
+ text.append(f"{title}\n\n", style="bold")
251
+ if lang_zh:
252
+ text.append(f"配置文件: {path}\n", style="grey62")
253
+ text.append("读取优先级: config.json > 环境变量 / 当前目录 .env > 默认 base URL\n\n", style="grey62")
254
+ else:
255
+ text.append(f"Config file: {path}\n", style="grey62")
256
+ text.append("Resolution order: config.json > environment variables / current .env > default base URL\n\n", style="grey62")
257
+
258
+ provider_order = list(config.provider_order)
259
+ for provider_name in _PROVIDER_SETUP_HELP:
260
+ if provider_name not in provider_order:
261
+ provider_order.append(provider_name)
262
+
263
+ for provider_name in provider_order:
264
+ help_info = _PROVIDER_SETUP_HELP.get(provider_name)
265
+ if not help_info:
266
+ continue
267
+ p_conf = config.providers.get(provider_name)
268
+ api_key = p_conf.api_key if p_conf else None
269
+ base_url = p_conf.base_url if p_conf and p_conf.base_url else help_info["default_url"]
270
+ enabled = p_conf.enabled if p_conf else True
271
+ status = "enabled" if api_key and enabled else ("disabled" if not enabled else "missing")
272
+ if lang_zh:
273
+ status_label = {"enabled": "已启用", "disabled": "已禁用", "missing": "缺少密钥"}[status]
274
+ text.append(f"{help_info['name']}\n", style="bold")
275
+ text.append(f" 状态: {status_label}\n", style="grey62")
276
+ text.append(f" Key: {_mask_secret(api_key)} ({' / '.join(help_info['key_envs'])})\n", style="grey62")
277
+ text.append(f" Base URL: {base_url} ({help_info['base_env']})\n", style="grey62")
278
+ text.append(f" 提示: {help_info['note_zh']}\n", style="grey62")
279
+ else:
280
+ text.append(f"{help_info['name']}\n", style="bold")
281
+ text.append(f" status: {status}\n", style="grey62")
282
+ text.append(f" key: {_mask_secret(api_key)} ({' / '.join(help_info['key_envs'])})\n", style="grey62")
283
+ text.append(f" baseUrl: {base_url} ({help_info['base_env']})\n", style="grey62")
284
+ text.append(f" note: {help_info['note_en']}\n", style="grey62")
285
+
286
+ if provider_name == "openrouter":
287
+ management_key = p_conf.management_key if p_conf else None
288
+ if lang_zh:
289
+ text.append(
290
+ f" 管理 Key: {_mask_secret(management_key)} (OPENROUTER_MANAGEMENT_KEY / OPENROUTER_ADMIN_KEY)\n",
291
+ style="grey62",
292
+ )
293
+ else:
294
+ text.append(
295
+ f" management key: {_mask_secret(management_key)} (OPENROUTER_MANAGEMENT_KEY / OPENROUTER_ADMIN_KEY)\n",
296
+ style="grey62",
297
+ )
298
+ text.append("\n")
299
+
300
+ if lang_zh:
301
+ text.append(
302
+ "建议把长期配置写入 ~/.kimi-usage/config.json;uvx 每次运行都会读取这个文件。\n",
303
+ style="grey62",
304
+ )
305
+ else:
306
+ text.append(
307
+ "For persistent uvx usage, put long-lived settings in ~/.kimi-usage/config.json.\n",
308
+ style="grey62",
309
+ )
310
+ return text
311
+
158
312
  # (typing already imported at top)
159
313
 
160
314
  THEME_MAP = {
@@ -824,7 +978,7 @@ def _format_aggregated_results(
824
978
  return result
825
979
 
826
980
 
827
- async def _interactive_mode(config: "AppConfig", initial_theme: str) -> None:
981
+ async def _interactive_mode(config: "AppConfig", initial_theme: str, config_path: str | None = None) -> None:
828
982
  """Live Rich TUI with keyboard theme cycling and provider panel toggling.
829
983
 
830
984
  Keys:
@@ -860,6 +1014,7 @@ async def _interactive_mode(config: "AppConfig", initial_theme: str) -> None:
860
1014
 
861
1015
  or_metric: str = _parse_or_metric(config.or_metric)
862
1016
  days_window: int = _parse_days_window(config.days_window)
1017
+ current_view = "usage"
863
1018
 
864
1019
  saved_notice: list = [None] # holds the saved theme name briefly, then None
865
1020
 
@@ -871,7 +1026,12 @@ async def _interactive_mode(config: "AppConfig", initial_theme: str) -> None:
871
1026
  def _build_panel() -> Panel:
872
1027
  visible_order = [p for p in config.provider_order if p in visible_providers]
873
1028
  _L = L_ZH if lang_zh else L_EN
874
- body = _format_aggregated_results(results, errors, visible_order, themes[idx], lang_zh, enabled_providers=set(config.enabled_providers), or_metric=or_metric, days_window=days_window)
1029
+ if current_view == "help":
1030
+ body = _render_interactive_help(lang_zh, or_metric=or_metric, days_window=days_window)
1031
+ elif current_view == "config":
1032
+ body = _render_config_guide(config, lang_zh=lang_zh, config_path=config_path)
1033
+ else:
1034
+ body = _format_aggregated_results(results, errors, visible_order, themes[idx], lang_zh, enabled_providers=set(config.enabled_providers), or_metric=or_metric, days_window=days_window)
875
1035
 
876
1036
  top_bar = Text()
877
1037
  # Provider toggles
@@ -894,6 +1054,8 @@ async def _interactive_mode(config: "AppConfig", initial_theme: str) -> None:
894
1054
  bindings = [
895
1055
  ("[q]", "退出" if lang_zh else "quit"),
896
1056
  ("[r]", "刷新" if lang_zh else "refresh"),
1057
+ ("[h/?]", "帮助" if lang_zh else "help"),
1058
+ ("[c]", "配置" if lang_zh else "config"),
897
1059
  ("[←/→]", "主题" if lang_zh else "theme"),
898
1060
  ("[1-4]", "面板" if lang_zh else "panels"),
899
1061
  ("[l]", "英" if lang_zh else "ZH"),
@@ -961,6 +1123,14 @@ async def _interactive_mode(config: "AppConfig", initial_theme: str) -> None:
961
1123
 
962
1124
  if saved_notice[0]: # clear notice on any new keypress
963
1125
  saved_notice[0] = None
1126
+ if ch in ('h', 'H', '?'):
1127
+ current_view = "usage" if current_view == "help" else "help"
1128
+ live.update(_build_panel())
1129
+ continue
1130
+ if ch in ('c', 'C'):
1131
+ current_view = "usage" if current_view == "config" else "config"
1132
+ live.update(_build_panel())
1133
+ continue
964
1134
  if ch in ('\r', '\n'): # Enter → save settings to config file
965
1135
  save_theme(
966
1136
  themes[idx],
@@ -1011,7 +1181,7 @@ async def main():
1011
1181
  parser.add_argument("--json", action="store_true")
1012
1182
  parser.add_argument("--plain", action="store_true")
1013
1183
  parser.add_argument("-i", "--interactive", action="store_true",
1014
- help="Interactive mode: ←/→ or [/] to cycle themes, r refresh, q quit")
1184
+ help="Interactive mode: h help, c config, ←/→ or [/] themes, r refresh, q quit")
1015
1185
  parser.add_argument("--provider", help="Comma-separated providers to query (kimi,openai,anthropic,openrouter)")
1016
1186
  parser.add_argument("--config", help="Custom configuration file path")
1017
1187
  parser.add_argument("--theme", help="Specify color theme: blue-dark, blue-light, sky-dark, salmon-dark, turquoise-dark, pink-light, violet-dark, amber-dark, mint-dark, monochrome, blind-deuteranopia, blind-tritanopia")
@@ -1034,7 +1204,7 @@ async def main():
1034
1204
 
1035
1205
  # Interactive mode: hand off to TUI loop (handles its own dispatch)
1036
1206
  if args.interactive:
1037
- await _interactive_mode(config, theme_name)
1207
+ await _interactive_mode(config, theme_name, config_path=str(resolver.config_path))
1038
1208
  return
1039
1209
 
1040
1210
  results, errors = await dispatch_all(config)
@@ -1,9 +1,8 @@
1
+ import os
1
2
  from collections.abc import Mapping, Sequence
2
3
  from datetime import datetime, timedelta
3
4
  from typing import Any, cast
4
5
 
5
- import os
6
-
7
6
  import aiohttp
8
7
 
9
8
  from . import ProviderUsage
@@ -148,6 +147,12 @@ def _kimi_error_hint(status: int, zh: bool | None = None) -> str:
148
147
  " ▸ 如需获取正确的 Key,请前往 Kimi Code 控制台创建\n"
149
148
  " ▸ 环境变量:KIMI_API_KEY 或 KIMI_CODING_API_KEY"
150
149
  ),
150
+ 404: (
151
+ "未找到 Kimi Code API 用量接口(404)。\n"
152
+ " ▸ 请确认 KIMI_BASE_URL/baseUrl 是 https://api.kimi.com/coding/v1\n"
153
+ " ▸ 请勿使用 Kimi 开放平台地址(https://api.moonshot.cn/v1 或 platform.kimi.com)\n"
154
+ " ▸ 请确认 API Key 来自 Kimi Code 平台(Coding Plan),格式为 sk-kimi-xxx"
155
+ ),
151
156
  403: "Kimi Code API 拒绝访问(403),请检查 API Key 是否有权限访问用量接口。",
152
157
  429: "Kimi Code API 请求过于频繁(429),请稍后重试。",
153
158
  }
@@ -160,6 +165,12 @@ def _kimi_error_hint(status: int, zh: bool | None = None) -> str:
160
165
  " ▸ Get the correct key from Kimi Code Console\n"
161
166
  " ▸ Environment variables: KIMI_API_KEY or KIMI_CODING_API_KEY"
162
167
  ),
168
+ 404: (
169
+ "Kimi Code API usage endpoint was not found (404).\n"
170
+ " ▸ Make sure KIMI_BASE_URL/baseUrl is https://api.kimi.com/coding/v1\n"
171
+ " ▸ Do NOT use the Kimi Open Platform base URL (https://api.moonshot.cn/v1 or platform.kimi.com)\n"
172
+ " ▸ Make sure the API Key is from Kimi Code Platform (Coding Plan), format: sk-kimi-xxx"
173
+ ),
163
174
  403: "Kimi Code API access denied (403). Check if your API Key has permission to access the usage endpoint.",
164
175
  429: "Kimi Code API rate limited (429). Please retry later.",
165
176
  }
@@ -170,7 +181,9 @@ async def fetch_kimi_usage(api_key: str, base_url: str, management_key: str | No
170
181
  headers = _kimi_headers(api_key)
171
182
  async with aiohttp.ClientSession() as session:
172
183
  async with session.get(url, headers=headers) as resp:
173
- if resp.status != 200:
184
+ if resp.status == 200:
185
+ payload = await resp.json()
186
+ elif resp.status == 404:
174
187
  fallback_url = base_url.rstrip("/") + "/usage"
175
188
  async with session.get(fallback_url, headers=headers) as f_resp:
176
189
  if f_resp.status != 200:
@@ -179,7 +192,9 @@ async def fetch_kimi_usage(api_key: str, base_url: str, management_key: str | No
179
192
  raise Exception(f"{hint}\n Original: API Error {f_resp.status}: {text}")
180
193
  payload = await f_resp.json()
181
194
  else:
182
- payload = await resp.json()
195
+ text = await resp.text()
196
+ hint = _kimi_error_hint(resp.status)
197
+ raise Exception(f"{hint}\n Original: API Error {resp.status}: {text}")
183
198
 
184
199
  summary, limits = _parse_usage_payload(payload)
185
200
  rows = ([summary] if summary else []) + limits
@@ -330,6 +330,16 @@ def _cfg_one_provider(m):
330
330
  return cfg
331
331
 
332
332
 
333
+ def _panel_plain(panel):
334
+ parts = []
335
+ for item in getattr(panel.renderable, "renderables", []):
336
+ if hasattr(item, "plain"):
337
+ parts.append(item.plain)
338
+ else:
339
+ parts.append(str(item))
340
+ return "\n".join(parts)
341
+
342
+
333
343
  @pytest.mark.parametrize("keys,theme,live_update_min,dispatch_call_count,cfg_maker", [
334
344
  (["q"], "blue-dark", 1, 1, None),
335
345
  (["r", "q"], "blue-dark", 2, 2, None),
@@ -355,6 +365,62 @@ def test_interactive_mode_initializes_from_config(monkeypatch, language):
355
365
  _run_interactive_mode(monkeypatch, ["q"], cfg=cfg, live_update_min=1, dispatch_call_count=1)
356
366
 
357
367
 
368
+ def test_render_config_guide_includes_supported_provider_setup():
369
+ from kimi_code_usage.config import AppConfig, ProviderConfig
370
+ from kimi_code_usage.main import _render_config_guide
371
+
372
+ cfg = AppConfig()
373
+ cfg.providers = {
374
+ "kimi": ProviderConfig(api_key="sk-kimi-1234567890", base_url="https://api.kimi.com/coding/v1"),
375
+ "openai": ProviderConfig(api_key="sk-openai-abcdef1234", base_url="https://api.openai.com"),
376
+ "anthropic": ProviderConfig(api_key=None, base_url="https://api.anthropic.com"),
377
+ "openrouter": ProviderConfig(api_key="sk-or-v1-abcdef1234", base_url="https://openrouter.ai/api", management_key="or-mgmt-1234"),
378
+ }
379
+ cfg.provider_order = ["kimi", "openai", "anthropic", "openrouter"]
380
+
381
+ text = _render_config_guide(cfg, lang_zh=False, config_path="/tmp/kimi-usage/config.json")
382
+ raw = text.plain
383
+
384
+ assert "/tmp/kimi-usage/config.json" in raw
385
+ assert "KIMI_API_KEY" in raw
386
+ assert "OPENAI_API_KEY" in raw
387
+ assert "ANTHROPIC_API_KEY" in raw
388
+ assert "OPENROUTER_API_KEY" in raw
389
+ assert "OPENROUTER_ADMIN_KEY" in raw
390
+ assert "OPENROUTER_MANAGEMENT_KEY" in raw
391
+ assert "https://api.kimi.com/coding/v1" in raw
392
+ assert "https://openrouter.ai/api" in raw
393
+ assert "https://api.moonshot.cn/v1" in raw
394
+ assert "sk-kimi" in raw
395
+ assert "sk-kimi-1234567890" not in raw
396
+ assert "or-mgmt-1234" not in raw
397
+ assert "missing" in raw
398
+
399
+
400
+ @pytest.mark.asyncio
401
+ async def test_interactive_mode_help_and_config_keys(monkeypatch):
402
+ _mock_terminal(monkeypatch)
403
+ mock_select, mock_read = _make_select_and_read(["h", "c", "q"])
404
+ import kimi_code_usage.main as main_mod
405
+ monkeypatch.setattr(main_mod._select_module, "select", mock_select)
406
+ monkeypatch.setattr(sys.stdin, "read", mock_read)
407
+
408
+ cfg = _make_interactive_config(monkeypatch)
409
+ mock_res = {"kimi": [ProviderUsage(provider="kimi", label="Weekly Usage", used=5, limit=100, remaining=95, percent=5, reset_at=None, unit="%")]}
410
+
411
+ with patch("kimi_code_usage.main.dispatch_all", AsyncMock(return_value=(mock_res, {}))):
412
+ with patch("kimi_code_usage.main.Live") as mock_live_cls:
413
+ mock_live = mock_live_cls.return_value.__enter__.return_value
414
+ await _interactive_mode(cfg, "blue-dark")
415
+
416
+ rendered = "\n".join(_panel_plain(call.args[0]) for call in mock_live.update.call_args_list)
417
+ assert "Interactive Help" in rendered
418
+ assert "Configuration Guide" in rendered
419
+ assert "OPENAI_API_KEY" in rendered
420
+ assert "ANTHROPIC_API_KEY" in rendered
421
+ assert "OPENROUTER_API_KEY" in rendered
422
+
423
+
358
424
  @pytest.mark.asyncio
359
425
  async def test_interactive_mode_lone_escape(monkeypatch):
360
426
  """A lone ESC with no follow-up → treated as unknown key (no-op), then q."""
@@ -5,6 +5,7 @@ import pytest
5
5
 
6
6
  from kimi_code_usage.providers.kimi import (
7
7
  _get_reset_info,
8
+ _kimi_error_hint,
8
9
  _limit_label,
9
10
  _parse_usage_payload,
10
11
  _to_int,
@@ -25,6 +26,13 @@ from kimi_code_usage.providers.kimi import (
25
26
  def test_to_int(val, expected):
26
27
  assert _to_int(val) == expected
27
28
 
29
+ def test_kimi_error_hint_404_explains_usage_endpoint_configuration():
30
+ hint = _kimi_error_hint(404, zh=False)
31
+ assert "usage endpoint" in hint
32
+ assert "https://api.kimi.com/coding/v1" in hint
33
+ assert "api.moonshot.cn" in hint
34
+ assert "sk-kimi" in hint
35
+
28
36
  # -- _get_reset_info --
29
37
  @pytest.mark.parametrize("key", ["resetTime", "reset_at", "reset_time"])
30
38
  def test_get_reset_info_iso_string(key):
@@ -188,6 +196,38 @@ async def test_fetch_kimi_usage_fallback():
188
196
  assert len(res) == 1
189
197
  assert res[0].used == 200.0
190
198
 
199
+ @pytest.mark.asyncio
200
+ async def test_fetch_kimi_usage_does_not_fallback_on_auth_error():
201
+ calls = []
202
+
203
+ def make_cm(status, text_data):
204
+ cm = AsyncMock()
205
+ cm.__aenter__ = AsyncMock(return_value=cm)
206
+ cm.__aexit__ = AsyncMock(return_value=None)
207
+ cm.status = status
208
+ cm.text = AsyncMock(return_value=text_data)
209
+ return cm
210
+
211
+ def mock_get(url, *a, **kw):
212
+ calls.append(url)
213
+ if len(calls) == 1:
214
+ return make_cm(401, '{"code":"unauthenticated"}')
215
+ return make_cm(404, '{"error":{"message":"The requested resource was not found","type":"resource_not_found_error"}}')
216
+
217
+ mock_session = AsyncMock()
218
+ mock_session.get = mock_get
219
+ mock_session.__aenter__ = AsyncMock(return_value=mock_session)
220
+ mock_session.__aexit__ = AsyncMock(return_value=None)
221
+
222
+ with patch('aiohttp.ClientSession', return_value=mock_session):
223
+ with pytest.raises(Exception) as exc:
224
+ await fetch_kimi_usage("test-key", "https://api.example.com/v1")
225
+
226
+ assert "401" in str(exc.value)
227
+ assert "unauthenticated" in str(exc.value)
228
+ assert "resource_not_found_error" not in str(exc.value)
229
+ assert calls == ["https://api.example.com/v1/usages"]
230
+
191
231
  @pytest.mark.asyncio
192
232
  async def test_fetch_kimi_usage_fallback_fails():
193
233
  def make_cm(status):
@@ -1049,7 +1049,7 @@ wheels = [
1049
1049
 
1050
1050
  [[package]]
1051
1051
  name = "kimi-code-usage"
1052
- version = "0.1.3"
1052
+ version = "0.1.5"
1053
1053
  source = { editable = "." }
1054
1054
  dependencies = [
1055
1055
  { name = "aiohttp" },