bhu-cli 1.46.0__py3-none-any.whl

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 (258) hide show
  1. bhu_cli-1.46.0.dist-info/METADATA +210 -0
  2. bhu_cli-1.46.0.dist-info/RECORD +258 -0
  3. bhu_cli-1.46.0.dist-info/WHEEL +4 -0
  4. bhu_cli-1.46.0.dist-info/entry_points.txt +4 -0
  5. kimi_cli/CHANGELOG.md +1281 -0
  6. kimi_cli/__init__.py +29 -0
  7. kimi_cli/__main__.py +43 -0
  8. kimi_cli/_build_info.py +1 -0
  9. kimi_cli/acp/AGENTS.md +92 -0
  10. kimi_cli/acp/__init__.py +13 -0
  11. kimi_cli/acp/convert.py +128 -0
  12. kimi_cli/acp/kaos.py +291 -0
  13. kimi_cli/acp/mcp.py +46 -0
  14. kimi_cli/acp/server.py +468 -0
  15. kimi_cli/acp/session.py +499 -0
  16. kimi_cli/acp/tools.py +167 -0
  17. kimi_cli/acp/types.py +13 -0
  18. kimi_cli/acp/version.py +45 -0
  19. kimi_cli/agents/default/agent.yaml +36 -0
  20. kimi_cli/agents/default/coder.yaml +25 -0
  21. kimi_cli/agents/default/explore.yaml +46 -0
  22. kimi_cli/agents/default/plan.yaml +30 -0
  23. kimi_cli/agents/default/system.md +160 -0
  24. kimi_cli/agents/okabe/agent.yaml +22 -0
  25. kimi_cli/agentspec.py +160 -0
  26. kimi_cli/app.py +807 -0
  27. kimi_cli/approval_runtime/__init__.py +29 -0
  28. kimi_cli/approval_runtime/models.py +42 -0
  29. kimi_cli/approval_runtime/runtime.py +235 -0
  30. kimi_cli/auth/__init__.py +5 -0
  31. kimi_cli/auth/oauth.py +1092 -0
  32. kimi_cli/auth/platforms.py +374 -0
  33. kimi_cli/background/__init__.py +36 -0
  34. kimi_cli/background/agent_runner.py +231 -0
  35. kimi_cli/background/ids.py +19 -0
  36. kimi_cli/background/manager.py +725 -0
  37. kimi_cli/background/models.py +105 -0
  38. kimi_cli/background/store.py +237 -0
  39. kimi_cli/background/summary.py +66 -0
  40. kimi_cli/background/worker.py +209 -0
  41. kimi_cli/cli/__init__.py +1081 -0
  42. kimi_cli/cli/__main__.py +34 -0
  43. kimi_cli/cli/_lazy_group.py +238 -0
  44. kimi_cli/cli/export.py +322 -0
  45. kimi_cli/cli/info.py +62 -0
  46. kimi_cli/cli/mcp.py +353 -0
  47. kimi_cli/cli/plugin.py +347 -0
  48. kimi_cli/cli/toad.py +73 -0
  49. kimi_cli/cli/vis.py +38 -0
  50. kimi_cli/cli/web.py +80 -0
  51. kimi_cli/config.py +429 -0
  52. kimi_cli/constant.py +116 -0
  53. kimi_cli/exception.py +43 -0
  54. kimi_cli/hooks/__init__.py +24 -0
  55. kimi_cli/hooks/config.py +34 -0
  56. kimi_cli/hooks/defaults/rtk.py +48 -0
  57. kimi_cli/hooks/engine.py +394 -0
  58. kimi_cli/hooks/events.py +190 -0
  59. kimi_cli/hooks/runner.py +103 -0
  60. kimi_cli/llm.py +333 -0
  61. kimi_cli/mcp_oauth.py +72 -0
  62. kimi_cli/metadata.py +79 -0
  63. kimi_cli/notifications/__init__.py +33 -0
  64. kimi_cli/notifications/llm.py +77 -0
  65. kimi_cli/notifications/manager.py +145 -0
  66. kimi_cli/notifications/models.py +50 -0
  67. kimi_cli/notifications/notifier.py +41 -0
  68. kimi_cli/notifications/store.py +118 -0
  69. kimi_cli/notifications/wire.py +21 -0
  70. kimi_cli/plugin/__init__.py +124 -0
  71. kimi_cli/plugin/manager.py +153 -0
  72. kimi_cli/plugin/tool.py +173 -0
  73. kimi_cli/prompts/__init__.py +6 -0
  74. kimi_cli/prompts/compact.md +73 -0
  75. kimi_cli/prompts/init.md +21 -0
  76. kimi_cli/py.typed +0 -0
  77. kimi_cli/session.py +319 -0
  78. kimi_cli/session_fork.py +325 -0
  79. kimi_cli/session_state.py +132 -0
  80. kimi_cli/share.py +14 -0
  81. kimi_cli/skill/__init__.py +727 -0
  82. kimi_cli/skill/flow/__init__.py +99 -0
  83. kimi_cli/skill/flow/d2.py +482 -0
  84. kimi_cli/skill/flow/mermaid.py +266 -0
  85. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  86. kimi_cli/skills/skill-creator/SKILL.md +367 -0
  87. kimi_cli/soul/__init__.py +304 -0
  88. kimi_cli/soul/agent.py +519 -0
  89. kimi_cli/soul/approval.py +267 -0
  90. kimi_cli/soul/btw.py +237 -0
  91. kimi_cli/soul/compaction.py +189 -0
  92. kimi_cli/soul/context.py +339 -0
  93. kimi_cli/soul/denwarenji.py +39 -0
  94. kimi_cli/soul/dynamic_injection.py +84 -0
  95. kimi_cli/soul/dynamic_injections/__init__.py +0 -0
  96. kimi_cli/soul/dynamic_injections/afk_mode.py +74 -0
  97. kimi_cli/soul/dynamic_injections/plan_mode.py +245 -0
  98. kimi_cli/soul/kimisoul.py +1710 -0
  99. kimi_cli/soul/message.py +92 -0
  100. kimi_cli/soul/slash.py +341 -0
  101. kimi_cli/soul/toolset.py +891 -0
  102. kimi_cli/subagents/__init__.py +21 -0
  103. kimi_cli/subagents/builder.py +42 -0
  104. kimi_cli/subagents/core.py +86 -0
  105. kimi_cli/subagents/git_context.py +170 -0
  106. kimi_cli/subagents/models.py +54 -0
  107. kimi_cli/subagents/output.py +71 -0
  108. kimi_cli/subagents/registry.py +28 -0
  109. kimi_cli/subagents/runner.py +428 -0
  110. kimi_cli/subagents/store.py +196 -0
  111. kimi_cli/telemetry/__init__.py +197 -0
  112. kimi_cli/telemetry/crash.py +149 -0
  113. kimi_cli/telemetry/sink.py +143 -0
  114. kimi_cli/telemetry/transport.py +318 -0
  115. kimi_cli/tools/AGENTS.md +5 -0
  116. kimi_cli/tools/__init__.py +105 -0
  117. kimi_cli/tools/agent/__init__.py +277 -0
  118. kimi_cli/tools/agent/description.md +41 -0
  119. kimi_cli/tools/ask_user/__init__.py +154 -0
  120. kimi_cli/tools/ask_user/description.md +19 -0
  121. kimi_cli/tools/background/__init__.py +318 -0
  122. kimi_cli/tools/background/list.md +10 -0
  123. kimi_cli/tools/background/output.md +11 -0
  124. kimi_cli/tools/background/stop.md +8 -0
  125. kimi_cli/tools/display.py +46 -0
  126. kimi_cli/tools/dmail/__init__.py +38 -0
  127. kimi_cli/tools/dmail/dmail.md +17 -0
  128. kimi_cli/tools/file/__init__.py +30 -0
  129. kimi_cli/tools/file/glob.md +21 -0
  130. kimi_cli/tools/file/glob.py +178 -0
  131. kimi_cli/tools/file/grep.md +6 -0
  132. kimi_cli/tools/file/grep_local.py +590 -0
  133. kimi_cli/tools/file/plan_mode.py +45 -0
  134. kimi_cli/tools/file/read.md +16 -0
  135. kimi_cli/tools/file/read.py +300 -0
  136. kimi_cli/tools/file/read_media.md +24 -0
  137. kimi_cli/tools/file/read_media.py +217 -0
  138. kimi_cli/tools/file/replace.md +7 -0
  139. kimi_cli/tools/file/replace.py +195 -0
  140. kimi_cli/tools/file/utils.py +257 -0
  141. kimi_cli/tools/file/write.md +5 -0
  142. kimi_cli/tools/file/write.py +177 -0
  143. kimi_cli/tools/plan/__init__.py +339 -0
  144. kimi_cli/tools/plan/description.md +29 -0
  145. kimi_cli/tools/plan/enter.py +197 -0
  146. kimi_cli/tools/plan/enter_description.md +35 -0
  147. kimi_cli/tools/plan/heroes.py +277 -0
  148. kimi_cli/tools/shell/__init__.py +260 -0
  149. kimi_cli/tools/shell/bash.md +35 -0
  150. kimi_cli/tools/test.py +55 -0
  151. kimi_cli/tools/think/__init__.py +21 -0
  152. kimi_cli/tools/think/think.md +1 -0
  153. kimi_cli/tools/todo/__init__.py +168 -0
  154. kimi_cli/tools/todo/set_todo_list.md +23 -0
  155. kimi_cli/tools/utils.py +199 -0
  156. kimi_cli/tools/web/__init__.py +4 -0
  157. kimi_cli/tools/web/fetch.md +1 -0
  158. kimi_cli/tools/web/fetch.py +189 -0
  159. kimi_cli/tools/web/search.md +1 -0
  160. kimi_cli/tools/web/search.py +163 -0
  161. kimi_cli/ui/__init__.py +0 -0
  162. kimi_cli/ui/acp/__init__.py +99 -0
  163. kimi_cli/ui/print/__init__.py +474 -0
  164. kimi_cli/ui/print/visualize.py +194 -0
  165. kimi_cli/ui/shell/__init__.py +1540 -0
  166. kimi_cli/ui/shell/console.py +109 -0
  167. kimi_cli/ui/shell/debug.py +190 -0
  168. kimi_cli/ui/shell/echo.py +17 -0
  169. kimi_cli/ui/shell/export_import.py +117 -0
  170. kimi_cli/ui/shell/keyboard.py +300 -0
  171. kimi_cli/ui/shell/mcp_status.py +111 -0
  172. kimi_cli/ui/shell/oauth.py +149 -0
  173. kimi_cli/ui/shell/placeholders.py +531 -0
  174. kimi_cli/ui/shell/prompt.py +2259 -0
  175. kimi_cli/ui/shell/replay.py +215 -0
  176. kimi_cli/ui/shell/session_picker.py +227 -0
  177. kimi_cli/ui/shell/setup.py +212 -0
  178. kimi_cli/ui/shell/slash.py +893 -0
  179. kimi_cli/ui/shell/startup.py +32 -0
  180. kimi_cli/ui/shell/task_browser.py +486 -0
  181. kimi_cli/ui/shell/update.py +349 -0
  182. kimi_cli/ui/shell/usage.py +295 -0
  183. kimi_cli/ui/shell/visualize/__init__.py +165 -0
  184. kimi_cli/ui/shell/visualize/_approval_panel.py +505 -0
  185. kimi_cli/ui/shell/visualize/_blocks.py +640 -0
  186. kimi_cli/ui/shell/visualize/_btw_panel.py +224 -0
  187. kimi_cli/ui/shell/visualize/_input_router.py +48 -0
  188. kimi_cli/ui/shell/visualize/_interactive.py +524 -0
  189. kimi_cli/ui/shell/visualize/_live_view.py +902 -0
  190. kimi_cli/ui/shell/visualize/_question_panel.py +586 -0
  191. kimi_cli/ui/theme.py +241 -0
  192. kimi_cli/utils/__init__.py +0 -0
  193. kimi_cli/utils/aiohttp.py +24 -0
  194. kimi_cli/utils/aioqueue.py +72 -0
  195. kimi_cli/utils/broadcast.py +37 -0
  196. kimi_cli/utils/changelog.py +108 -0
  197. kimi_cli/utils/clipboard.py +246 -0
  198. kimi_cli/utils/datetime.py +64 -0
  199. kimi_cli/utils/diff.py +135 -0
  200. kimi_cli/utils/editor.py +91 -0
  201. kimi_cli/utils/environment.py +225 -0
  202. kimi_cli/utils/envvar.py +22 -0
  203. kimi_cli/utils/export.py +696 -0
  204. kimi_cli/utils/file_filter.py +367 -0
  205. kimi_cli/utils/frontmatter.py +70 -0
  206. kimi_cli/utils/io.py +27 -0
  207. kimi_cli/utils/logging.py +124 -0
  208. kimi_cli/utils/media_tags.py +29 -0
  209. kimi_cli/utils/message.py +24 -0
  210. kimi_cli/utils/path.py +245 -0
  211. kimi_cli/utils/proctitle.py +33 -0
  212. kimi_cli/utils/proxy.py +31 -0
  213. kimi_cli/utils/pyinstaller.py +43 -0
  214. kimi_cli/utils/rich/__init__.py +33 -0
  215. kimi_cli/utils/rich/columns.py +99 -0
  216. kimi_cli/utils/rich/diff_render.py +481 -0
  217. kimi_cli/utils/rich/markdown.py +898 -0
  218. kimi_cli/utils/rich/markdown_sample.md +108 -0
  219. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  220. kimi_cli/utils/rich/syntax.py +114 -0
  221. kimi_cli/utils/sensitive.py +54 -0
  222. kimi_cli/utils/server.py +121 -0
  223. kimi_cli/utils/shell_quoting.py +38 -0
  224. kimi_cli/utils/signals.py +43 -0
  225. kimi_cli/utils/slashcmd.py +145 -0
  226. kimi_cli/utils/string.py +41 -0
  227. kimi_cli/utils/subprocess_env.py +73 -0
  228. kimi_cli/utils/term.py +168 -0
  229. kimi_cli/utils/typing.py +20 -0
  230. kimi_cli/utils/windows_paths.py +48 -0
  231. kimi_cli/vis/__init__.py +0 -0
  232. kimi_cli/vis/api/__init__.py +5 -0
  233. kimi_cli/vis/api/sessions.py +687 -0
  234. kimi_cli/vis/api/statistics.py +209 -0
  235. kimi_cli/vis/api/system.py +19 -0
  236. kimi_cli/vis/app.py +175 -0
  237. kimi_cli/web/__init__.py +5 -0
  238. kimi_cli/web/api/__init__.py +15 -0
  239. kimi_cli/web/api/config.py +208 -0
  240. kimi_cli/web/api/open_in.py +197 -0
  241. kimi_cli/web/api/sessions.py +1223 -0
  242. kimi_cli/web/app.py +451 -0
  243. kimi_cli/web/auth.py +191 -0
  244. kimi_cli/web/models.py +98 -0
  245. kimi_cli/web/runner/__init__.py +5 -0
  246. kimi_cli/web/runner/messages.py +57 -0
  247. kimi_cli/web/runner/process.py +754 -0
  248. kimi_cli/web/runner/worker.py +95 -0
  249. kimi_cli/web/store/__init__.py +1 -0
  250. kimi_cli/web/store/sessions.py +432 -0
  251. kimi_cli/wire/__init__.py +148 -0
  252. kimi_cli/wire/file.py +151 -0
  253. kimi_cli/wire/jsonrpc.py +263 -0
  254. kimi_cli/wire/protocol.py +2 -0
  255. kimi_cli/wire/root_hub.py +27 -0
  256. kimi_cli/wire/serde.py +26 -0
  257. kimi_cli/wire/server.py +1060 -0
  258. kimi_cli/wire/types.py +717 -0
@@ -0,0 +1,108 @@
1
+ # Markdown Sample Document
2
+
3
+ This is a comprehensive sample document showcasing various Markdown elements.
4
+
5
+ ## Level 2 Heading
6
+
7
+ ### Level 3 Heading
8
+
9
+ Here's some regular text with **bold text**, *italic text*, and `inline code`.
10
+
11
+ ## Lists
12
+
13
+ ### Unordered List
14
+
15
+ - First item
16
+ - Second item
17
+ - Nested item 1
18
+ - Nested item 2
19
+ - Third item
20
+
21
+ ### Ordered List
22
+
23
+ 1. First step
24
+ 2. Second step
25
+ 1. Sub-step A
26
+ 2. Sub-step B
27
+ 3. Third step
28
+
29
+ ### Mixed List
30
+
31
+ 1. First item
32
+ - Sub-item with bullet
33
+ - Another sub-item
34
+ 2. Second item
35
+ 1. Numbered sub-item
36
+ 2. Another numbered sub-item
37
+
38
+ ## Links and References
39
+
40
+ Here's a [link to GitHub](https://github.com) and another [relative link](../README.md).
41
+
42
+ ## Code Blocks
43
+
44
+ ```python
45
+ def hello_world():
46
+ """A simple function to demonstrate code blocks."""
47
+ print("Hello, World!")
48
+ return 42
49
+
50
+ # Call the function
51
+ result = hello_world()
52
+ ```
53
+
54
+ ```bash
55
+ # Bash example
56
+ echo "This is a bash script"
57
+ ls -la /tmp
58
+ ```
59
+
60
+ ## Blockquotes
61
+
62
+ > This is a blockquote.
63
+ > It can span multiple lines.
64
+ >
65
+ > > And it can be nested too!
66
+
67
+ ## Tables
68
+
69
+ | Column 1 | Column 2 | Column 3 |
70
+ |----------|----------|----------|
71
+ | Cell 1 | Cell 2 | Cell 3 |
72
+ | Left | Center | Right |
73
+ | Foo | Bar | Baz |
74
+
75
+ ## Horizontal Rules
76
+
77
+ ---
78
+
79
+ Here's some text after a horizontal rule.
80
+
81
+ ---
82
+
83
+ ## Inline Formatting
84
+
85
+ You can combine **bold and *italic*** text, or use `code` within paragraphs.
86
+
87
+ **Important**: Always test your `code` snippets before deployment.
88
+
89
+ ## Advanced Features
90
+
91
+ ### Task Lists
92
+
93
+ - [x] Completed task
94
+ - [ ] Pending task
95
+ - [ ] Another pending task
96
+
97
+ ### Definition Lists
98
+
99
+ Term 1
100
+ : Definition of term 1
101
+
102
+ Term 2
103
+ : Definition of term 2
104
+ : Another definition for term 2
105
+
106
+ ---
107
+
108
+ *This document demonstrates comprehensive Markdown formatting capabilities.*
@@ -0,0 +1,2 @@
1
+ - First
2
+ - Second
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pygments.token import (
6
+ Comment,
7
+ Generic,
8
+ Keyword,
9
+ Name,
10
+ Number,
11
+ Operator,
12
+ Punctuation,
13
+ String,
14
+ )
15
+ from pygments.token import (
16
+ Literal as PygmentsLiteral,
17
+ )
18
+ from pygments.token import (
19
+ Text as PygmentsText,
20
+ )
21
+ from pygments.token import (
22
+ Token as PygmentsToken,
23
+ )
24
+ from rich.style import Style
25
+ from rich.syntax import ANSISyntaxTheme, Syntax, SyntaxTheme
26
+
27
+ KIMI_ANSI_THEME_NAME = "kimi-ansi"
28
+ KIMI_ANSI_THEME = ANSISyntaxTheme(
29
+ {
30
+ PygmentsToken: Style(color="default"),
31
+ PygmentsText: Style(color="default"),
32
+ Comment: Style(color="bright_black", italic=True),
33
+ Keyword: Style(color="magenta"),
34
+ Keyword.Constant: Style(color="cyan"),
35
+ Keyword.Declaration: Style(color="magenta"),
36
+ Keyword.Namespace: Style(color="magenta"),
37
+ Keyword.Pseudo: Style(color="magenta"),
38
+ Keyword.Reserved: Style(color="magenta"),
39
+ Keyword.Type: Style(color="magenta"),
40
+ Name: Style(color="default"),
41
+ Name.Attribute: Style(color="cyan"),
42
+ Name.Builtin: Style(color="bright_yellow"),
43
+ Name.Builtin.Pseudo: Style(color="cyan"),
44
+ Name.Builtin.Type: Style(color="bright_yellow", bold=True),
45
+ Name.Class: Style(color="bright_yellow", bold=True),
46
+ Name.Constant: Style(color="cyan"),
47
+ Name.Decorator: Style(color="bright_cyan"),
48
+ Name.Entity: Style(color="bright_yellow"),
49
+ Name.Exception: Style(color="bright_yellow", bold=True),
50
+ Name.Function: Style(color="bright_cyan"),
51
+ Name.Label: Style(color="cyan"),
52
+ Name.Namespace: Style(color="magenta"),
53
+ Name.Other: Style(color="bright_cyan"),
54
+ Name.Property: Style(color="cyan"),
55
+ Name.Tag: Style(color="bright_green"),
56
+ Name.Variable: Style(color="bright_yellow"),
57
+ PygmentsLiteral: Style(color="bright_blue"),
58
+ PygmentsLiteral.Date: Style(color="bright_blue"),
59
+ String: Style(color="bright_blue"),
60
+ String.Doc: Style(color="bright_blue", italic=True),
61
+ String.Interpol: Style(color="bright_blue"),
62
+ String.Affix: Style(color="cyan"),
63
+ Number: Style(color="cyan"),
64
+ Operator: Style(color="default"),
65
+ Operator.Word: Style(color="magenta"),
66
+ Punctuation: Style(color="default"),
67
+ Generic.Deleted: Style(color="red"),
68
+ Generic.Emph: Style(italic=True),
69
+ Generic.Error: Style(color="bright_red", bold=True),
70
+ Generic.Heading: Style(color="cyan", bold=True),
71
+ Generic.Inserted: Style(color="green"),
72
+ Generic.Output: Style(color="bright_black"),
73
+ Generic.Prompt: Style(color="bright_cyan"),
74
+ Generic.Strong: Style(bold=True),
75
+ Generic.Subheading: Style(color="cyan"),
76
+ Generic.Traceback: Style(color="bright_red", bold=True),
77
+ }
78
+ )
79
+
80
+
81
+ def resolve_code_theme(theme: str | SyntaxTheme) -> str | SyntaxTheme:
82
+ if isinstance(theme, str) and theme.lower() == KIMI_ANSI_THEME_NAME:
83
+ return KIMI_ANSI_THEME
84
+ return theme
85
+
86
+
87
+ class KimiSyntax(Syntax):
88
+ def __init__(self, code: str, lexer: str, **kwargs: Any) -> None:
89
+ if "theme" not in kwargs or kwargs["theme"] is None:
90
+ kwargs["theme"] = KIMI_ANSI_THEME
91
+ super().__init__(code, lexer, **kwargs)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ from rich.console import Console
96
+ from rich.text import Text
97
+
98
+ console = Console()
99
+
100
+ examples = [
101
+ ("diff", "diff", "@@ -1,2 +1,2 @@\n-line one\n+line uno\n"),
102
+ (
103
+ "python",
104
+ "python",
105
+ 'def greet(name: str) -> str:\n return f"Hi, {name}!"\n',
106
+ ),
107
+ ("bash", "bash", "set -euo pipefail\nprintf '%s\\n' \"hello\"\n"),
108
+ ]
109
+
110
+ for idx, (title, lexer, code) in enumerate(examples):
111
+ if idx:
112
+ console.print()
113
+ console.print(Text(f"[{title}]", style="bold"))
114
+ console.print(KimiSyntax(code, lexer))
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ from pathlib import PurePath
5
+
6
+ # High-confidence sensitive file patterns.
7
+ # Only patterns with very low false-positive risk are included.
8
+ SENSITIVE_PATTERNS: list[str] = [
9
+ # Environment variable / secrets
10
+ ".env",
11
+ ".env.*",
12
+ # SSH private keys
13
+ "id_rsa",
14
+ "id_ed25519",
15
+ "id_ecdsa",
16
+ # Cloud credentials (path-based, also bare name for stripped-path scenarios)
17
+ ".aws/credentials",
18
+ ".gcp/credentials",
19
+ "credentials",
20
+ ]
21
+
22
+ # Template/example files that match .env.* but are not sensitive.
23
+ SENSITIVE_EXEMPTIONS: set[str] = {
24
+ ".env.example",
25
+ ".env.sample",
26
+ ".env.template",
27
+ }
28
+
29
+
30
+ def is_sensitive_file(path: str) -> bool:
31
+ """Check if a file path matches any sensitive file pattern."""
32
+ name = PurePath(path).name
33
+ if name in SENSITIVE_EXEMPTIONS:
34
+ return False
35
+ for pattern in SENSITIVE_PATTERNS:
36
+ if "/" in pattern:
37
+ if path.endswith(pattern) or ("/" + pattern) in path:
38
+ return True
39
+ else:
40
+ if fnmatch.fnmatch(name, pattern):
41
+ return True
42
+ return False
43
+
44
+
45
+ def sensitive_file_warning(paths: list[str]) -> str:
46
+ """Generate a warning message for sensitive files that were skipped."""
47
+ names = sorted({PurePath(p).name for p in paths})
48
+ file_list = ", ".join(names[:5])
49
+ if len(names) > 5:
50
+ file_list += f", ... ({len(names)} files total)"
51
+ return (
52
+ f"Skipped {len(paths)} sensitive file(s) ({file_list}) "
53
+ f"to protect secrets. These files may contain credentials or private keys."
54
+ )
@@ -0,0 +1,121 @@
1
+ """Shared utilities for kimi vis and kimi web server startup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import socket
7
+ import textwrap
8
+
9
+
10
+ def get_address_family(host: str) -> socket.AddressFamily:
11
+ """Return AF_INET6 for IPv6 addresses, AF_INET for IPv4 and hostnames."""
12
+ return socket.AF_INET6 if ":" in host else socket.AF_INET
13
+
14
+
15
+ def format_url(host: str, port: int) -> str:
16
+ """Build ``http://host:port``, bracketing IPv6 literals per RFC 2732."""
17
+ if ":" in host:
18
+ return f"http://[{host}]:{port}"
19
+ return f"http://{host}:{port}"
20
+
21
+
22
+ def is_local_host(host: str) -> bool:
23
+ """Check whether *host* resolves to a loopback address."""
24
+ return host in {"127.0.0.1", "localhost", "::1"}
25
+
26
+
27
+ def find_available_port(host: str, start_port: int, max_attempts: int = 10) -> int:
28
+ """Find an available port starting from *start_port*.
29
+
30
+ Raises ``RuntimeError`` if no port is available within the range.
31
+ """
32
+ if max_attempts <= 0:
33
+ raise ValueError("max_attempts must be positive")
34
+ if start_port < 1 or start_port > 65535:
35
+ raise ValueError("start_port must be between 1 and 65535")
36
+
37
+ family = get_address_family(host)
38
+ for offset in range(max_attempts):
39
+ port = start_port + offset
40
+ with socket.socket(family, socket.SOCK_STREAM) as s:
41
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
42
+ try:
43
+ s.bind((host, port))
44
+ return port
45
+ except OSError:
46
+ continue
47
+ raise RuntimeError(
48
+ f"Cannot find available port in range {start_port}-{start_port + max_attempts - 1}"
49
+ )
50
+
51
+
52
+ def get_network_addresses() -> list[str]:
53
+ """Get non-loopback IPv4 addresses for this machine."""
54
+ addresses: list[str] = []
55
+
56
+ try:
57
+ hostname = socket.gethostname()
58
+ for info in socket.getaddrinfo(hostname, None, socket.AF_INET):
59
+ ip = info[4][0]
60
+ if isinstance(ip, str) and not ip.startswith("127.") and ip not in addresses:
61
+ addresses.append(ip)
62
+ except OSError:
63
+ pass
64
+
65
+ try:
66
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
67
+ s.connect(("8.8.8.8", 80))
68
+ ip = s.getsockname()[0]
69
+ if ip and not ip.startswith("127.") and ip not in addresses:
70
+ addresses.append(ip)
71
+ except OSError:
72
+ pass
73
+
74
+ try:
75
+ netifaces = importlib.import_module("netifaces")
76
+ for interface in netifaces.interfaces():
77
+ addrs = netifaces.ifaddresses(interface)
78
+ if netifaces.AF_INET in addrs:
79
+ for addr_info in addrs[netifaces.AF_INET]:
80
+ addr = addr_info.get("addr")
81
+ if addr and not addr.startswith("127.") and addr not in addresses:
82
+ addresses.append(addr)
83
+ except (ImportError, Exception):
84
+ pass
85
+
86
+ return addresses
87
+
88
+
89
+ def print_banner(lines: list[str]) -> None:
90
+ """Print a boxed banner with tag conventions (<center>, <nowrap>, <hr>)."""
91
+ processed: list[str] = []
92
+ for line in lines:
93
+ if line == "<hr>":
94
+ processed.append(line)
95
+ elif not line:
96
+ processed.append("")
97
+ elif line.startswith("<center>") or line.startswith("<nowrap>"):
98
+ processed.append(line)
99
+ else:
100
+ processed.extend(textwrap.wrap(line, width=78))
101
+
102
+ def strip_tags(s: str) -> str:
103
+ return s.removeprefix("<center>").removeprefix("<nowrap>")
104
+
105
+ content_lines = [strip_tags(line) for line in processed if line != "<hr>"]
106
+ width = max(60, *(len(line) for line in content_lines))
107
+ top = "+" + "=" * (width + 2) + "+"
108
+
109
+ print(top)
110
+ for line in processed:
111
+ if line == "<hr>":
112
+ print("|" + "-" * (width + 2) + "|")
113
+ elif line.startswith("<center>"):
114
+ content = line.removeprefix("<center>")
115
+ print(f"| {content.center(width)} |")
116
+ elif line.startswith("<nowrap>"):
117
+ content = line.removeprefix("<nowrap>")
118
+ print(f"| {content.ljust(width)} |")
119
+ else:
120
+ print(f"| {line.ljust(width)} |")
121
+ print(top)
@@ -0,0 +1,38 @@
1
+ """Defensive rewrites applied to shell commands before execution.
2
+
3
+ The model occasionally hallucinates Windows CMD syntax even when running on
4
+ git-bash (which is POSIX). One such hallucination has caused real damage:
5
+ ``cmd 2>nul`` in git-bash creates a literal file named ``nul`` in cwd, which
6
+ is a Windows reserved device name that breaks ``git add .`` / ``git clone``.
7
+
8
+ The fix here mirrors claude-code's ``rewriteWindowsNullRedirect``
9
+ (see anthropics/claude-code#4928): rewrite the bad redirect to ``/dev/null``
10
+ before the command reaches the shell.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+
17
+ # Match `>nul`, `> NUL`, `2>nul`, `&>nul`, `>>nul` (case-insensitive),
18
+ # but NOT `>null`, `>nullable`, `>nul.txt`, `cat nul.txt`.
19
+ #
20
+ # Group 1 captures the redirect operator + optional whitespace, so the rewrite
21
+ # preserves the original spacing (e.g. `2> nul` -> `2> /dev/null`).
22
+ _NUL_REDIRECT = re.compile(r"(\d?&?>+\s*)[Nn][Uu][Ll](?=\s|$|[|&;)\n])")
23
+
24
+
25
+ def rewrite_windows_null_redirect(command: str, *, on_windows: bool) -> str:
26
+ """Rewrite Windows-style ``>nul`` redirects to POSIX ``/dev/null``.
27
+
28
+ Only active when ``on_windows`` is True. On Linux/macOS, ``>nul`` is a
29
+ legitimate redirect to a file named ``nul`` and must not be rewritten.
30
+
31
+ The regex's lookahead requires a shell-meaningful character after ``nul``
32
+ (whitespace, end-of-string, or one of ``|&;)\\n``), so quoted forms like
33
+ ``echo ">nul"`` slip through unmolested — a useful happy accident, since
34
+ rewriting inside string literals would corrupt user data.
35
+ """
36
+ if not on_windows:
37
+ return command
38
+ return _NUL_REDIRECT.sub(r"\1/dev/null", command)
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import signal
6
+ from collections.abc import Callable
7
+
8
+
9
+ def install_sigint_handler(
10
+ loop: asyncio.AbstractEventLoop, handler: Callable[[], None]
11
+ ) -> Callable[[], None]:
12
+ """
13
+ Install a SIGINT handler that works on Unix and Windows.
14
+
15
+ On Unix event loops, prefer `loop.add_signal_handler`.
16
+ On Windows (or other platforms) where it is not implemented, fall back to
17
+ `signal.signal`. The fallback cannot be removed from the loop, but we
18
+ restore the previous handler on uninstall.
19
+
20
+ Returns:
21
+ A function that removes the installed handler. It is guaranteed that
22
+ no exceptions are raised when calling the returned function.
23
+ """
24
+
25
+ try:
26
+ loop.add_signal_handler(signal.SIGINT, handler)
27
+
28
+ def remove() -> None:
29
+ with contextlib.suppress(RuntimeError):
30
+ loop.remove_signal_handler(signal.SIGINT)
31
+
32
+ return remove
33
+ except RuntimeError:
34
+ # Windows ProactorEventLoop and some environments do not support
35
+ # add_signal_handler. Use synchronous signal handling as a fallback.
36
+ previous = signal.getsignal(signal.SIGINT)
37
+ signal.signal(signal.SIGINT, lambda signum, frame: handler())
38
+
39
+ def remove() -> None:
40
+ with contextlib.suppress(RuntimeError):
41
+ signal.signal(signal.SIGINT, previous)
42
+
43
+ return remove
@@ -0,0 +1,145 @@
1
+ import re
2
+ from collections.abc import Awaitable, Callable, Sequence
3
+ from dataclasses import dataclass
4
+ from typing import overload
5
+
6
+
7
+ @dataclass(frozen=True, slots=True, kw_only=True)
8
+ class SlashCommand[F: Callable[..., None | Awaitable[None]]]:
9
+ name: str
10
+ description: str
11
+ func: F
12
+ aliases: list[str]
13
+
14
+ def display_name(self, trigger: str | None = None) -> str:
15
+ """/name for canonical triggers, /name (alias) for alias triggers."""
16
+ if trigger is not None and trigger != self.name and trigger in self.aliases:
17
+ return f"/{self.name} ({trigger})"
18
+ return f"/{self.name}"
19
+
20
+ def slash_name(self):
21
+ """/name (aliases)"""
22
+ if self.aliases:
23
+ return f"/{self.name} ({', '.join(self.aliases)})"
24
+ return f"/{self.name}"
25
+
26
+
27
+ class SlashCommandRegistry[F: Callable[..., None | Awaitable[None]]]:
28
+ """Registry for slash commands."""
29
+
30
+ def __init__(self) -> None:
31
+ self._commands: dict[str, SlashCommand[F]] = {}
32
+ """Primary name -> SlashCommand"""
33
+ self._command_aliases: dict[str, SlashCommand[F]] = {}
34
+ """Primary name or alias -> SlashCommand"""
35
+
36
+ @overload
37
+ def command(self, func: F, /) -> F: ...
38
+
39
+ @overload
40
+ def command(
41
+ self,
42
+ *,
43
+ name: str | None = None,
44
+ aliases: Sequence[str] | None = None,
45
+ ) -> Callable[[F], F]: ...
46
+
47
+ def command(
48
+ self,
49
+ func: F | None = None,
50
+ *,
51
+ name: str | None = None,
52
+ aliases: Sequence[str] | None = None,
53
+ ) -> F | Callable[[F], F]:
54
+ """
55
+ Decorator to register a slash command with optional custom name and aliases.
56
+
57
+ Usage examples:
58
+ @registry.command
59
+ def help(app: App, args: str): ...
60
+
61
+ @registry.command(name="run")
62
+ def start(app: App, args: str): ...
63
+
64
+ @registry.command(aliases=["h", "?", "assist"])
65
+ def help(app: App, args: str): ...
66
+ """
67
+
68
+ def _register(f: F) -> F:
69
+ primary = name or f.__name__
70
+ alias_list = list(aliases) if aliases else []
71
+
72
+ # Create the primary command with aliases
73
+ cmd = SlashCommand[F](
74
+ name=primary,
75
+ description=(f.__doc__ or "").strip(),
76
+ func=f,
77
+ aliases=alias_list,
78
+ )
79
+
80
+ # Register primary command
81
+ self._commands[primary] = cmd
82
+ self._command_aliases[primary] = cmd
83
+
84
+ # Register aliases pointing to the same command
85
+ for alias in alias_list:
86
+ self._command_aliases[alias] = cmd
87
+
88
+ return f
89
+
90
+ if func is not None:
91
+ return _register(func)
92
+ return _register
93
+
94
+ def find_command(self, name: str) -> SlashCommand[F] | None:
95
+ return self._command_aliases.get(name)
96
+
97
+ def list_commands(self) -> list[SlashCommand[F]]:
98
+ """Get all unique primary slash commands (without duplicating aliases)."""
99
+ return list(self._commands.values())
100
+
101
+ def iter_command_entries(self) -> list[tuple[str, SlashCommand[F]]]:
102
+ """Get canonical and alias entries as (trigger, command) pairs."""
103
+ entries: list[tuple[str, SlashCommand[F]]] = []
104
+ for cmd in self._commands.values():
105
+ entries.append((cmd.name, cmd))
106
+ seen = {cmd.name}
107
+ for alias in cmd.aliases:
108
+ if alias in seen:
109
+ continue
110
+ if self._command_aliases.get(alias) is not cmd:
111
+ continue
112
+ entries.append((alias, cmd))
113
+ seen.add(alias)
114
+ return entries
115
+
116
+
117
+ @dataclass(frozen=True, slots=True, kw_only=True)
118
+ class SlashCommandCall:
119
+ name: str
120
+ args: str
121
+ raw_input: str
122
+
123
+
124
+ def parse_slash_command_call(user_input: str) -> SlashCommandCall | None:
125
+ """
126
+ Parse a slash command call from user input.
127
+
128
+ Returns:
129
+ SlashCommandCall if a slash command is found, else None. The `args` field contains
130
+ the raw argument string after the command name.
131
+ """
132
+ user_input = user_input.strip()
133
+ if not user_input or not user_input.startswith("/"):
134
+ return None
135
+
136
+ name_match = re.match(r"^\/([a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)*)", user_input)
137
+
138
+ if not name_match:
139
+ return None
140
+
141
+ command_name = name_match.group(1)
142
+ if len(user_input) > name_match.end() and not user_input[name_match.end()].isspace():
143
+ return None
144
+ raw_args = user_input[name_match.end() :].lstrip()
145
+ return SlashCommandCall(name=command_name, args=raw_args, raw_input=user_input)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ import re
5
+ import string
6
+
7
+ _NEWLINE_RE = re.compile(r"[\r\n]+")
8
+
9
+
10
+ def shorten(text: str, *, width: int, placeholder: str = "…") -> str:
11
+ """Shorten text to at most *width* characters.
12
+
13
+ Normalises whitespace, then truncates — preferring a word boundary
14
+ when one exists near the cut point, but falling back to a hard cut
15
+ so that CJK text without spaces won't collapse to just the placeholder.
16
+ """
17
+ text = " ".join(text.split())
18
+ if len(text) <= width:
19
+ return text
20
+ cut = width - len(placeholder)
21
+ if cut <= 0:
22
+ return text[:width]
23
+ space = text.rfind(" ", 0, cut + 1)
24
+ if space > 0:
25
+ cut = space
26
+ return text[:cut].rstrip() + placeholder
27
+
28
+
29
+ def shorten_middle(text: str, width: int, remove_newline: bool = True) -> str:
30
+ """Shorten the text by inserting ellipsis in the middle."""
31
+ if len(text) <= width:
32
+ return text
33
+ if remove_newline:
34
+ text = _NEWLINE_RE.sub(" ", text)
35
+ return text[: width // 2] + "..." + text[-width // 2 :]
36
+
37
+
38
+ def random_string(length: int = 8) -> str:
39
+ """Generate a random string of fixed length."""
40
+ letters = string.ascii_lowercase
41
+ return "".join(random.choice(letters) for _ in range(length))