kimi-cli 0.35__py3-none-any.whl → 0.52__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.
- kimi_cli/CHANGELOG.md +165 -0
- kimi_cli/__init__.py +0 -374
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/app.py +208 -0
- kimi_cli/cli.py +321 -0
- kimi_cli/config.py +33 -16
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +144 -3
- kimi_cli/metadata.py +6 -69
- kimi_cli/prompts/__init__.py +4 -0
- kimi_cli/session.py +103 -0
- kimi_cli/soul/__init__.py +130 -9
- kimi_cli/soul/agent.py +159 -0
- kimi_cli/soul/approval.py +5 -6
- kimi_cli/soul/compaction.py +106 -0
- kimi_cli/soul/context.py +1 -1
- kimi_cli/soul/kimisoul.py +180 -80
- kimi_cli/soul/message.py +6 -6
- kimi_cli/soul/runtime.py +96 -0
- kimi_cli/soul/toolset.py +3 -2
- kimi_cli/tools/__init__.py +35 -31
- kimi_cli/tools/bash/__init__.py +25 -9
- kimi_cli/tools/bash/cmd.md +31 -0
- kimi_cli/tools/dmail/__init__.py +5 -4
- kimi_cli/tools/file/__init__.py +8 -0
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +4 -4
- kimi_cli/tools/file/grep.py +36 -19
- kimi_cli/tools/file/patch.py +52 -10
- kimi_cli/tools/file/read.py +6 -5
- kimi_cli/tools/file/replace.py +16 -4
- kimi_cli/tools/file/write.py +16 -4
- kimi_cli/tools/mcp.py +7 -4
- kimi_cli/tools/task/__init__.py +60 -41
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +4 -2
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/fetch.py +2 -1
- kimi_cli/tools/web/search.py +13 -12
- kimi_cli/ui/__init__.py +0 -68
- kimi_cli/ui/acp/__init__.py +67 -38
- kimi_cli/ui/print/__init__.py +46 -69
- kimi_cli/ui/shell/__init__.py +145 -154
- kimi_cli/ui/shell/console.py +27 -1
- kimi_cli/ui/shell/debug.py +187 -0
- kimi_cli/ui/shell/keyboard.py +183 -0
- kimi_cli/ui/shell/metacmd.py +34 -81
- kimi_cli/ui/shell/prompt.py +245 -28
- kimi_cli/ui/shell/replay.py +104 -0
- kimi_cli/ui/shell/setup.py +19 -19
- kimi_cli/ui/shell/update.py +11 -5
- kimi_cli/ui/shell/visualize.py +576 -0
- kimi_cli/ui/wire/README.md +109 -0
- kimi_cli/ui/wire/__init__.py +340 -0
- kimi_cli/ui/wire/jsonrpc.py +48 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +10 -0
- kimi_cli/utils/changelog.py +6 -2
- kimi_cli/utils/clipboard.py +10 -0
- kimi_cli/utils/message.py +15 -1
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/markdown.py +959 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +41 -0
- kimi_cli/utils/string.py +8 -0
- kimi_cli/utils/term.py +114 -0
- kimi_cli/wire/__init__.py +73 -0
- kimi_cli/wire/message.py +191 -0
- kimi_cli-0.52.dist-info/METADATA +186 -0
- kimi_cli-0.52.dist-info/RECORD +99 -0
- kimi_cli-0.52.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- kimi_cli/prompts/metacmds/__init__.py +0 -4
- kimi_cli/soul/wire.py +0 -101
- kimi_cli/ui/shell/liveview.py +0 -158
- kimi_cli/utils/provider.py +0 -64
- kimi_cli-0.35.dist-info/METADATA +0 -24
- kimi_cli-0.35.dist-info/RECORD +0 -76
- kimi_cli-0.35.dist-info/entry_points.txt +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- /kimi_cli/prompts/{metacmds/compact.md → compact.md} +0 -0
- /kimi_cli/prompts/{metacmds/init.md → init.md} +0 -0
- {kimi_cli-0.35.dist-info → kimi_cli-0.52.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import AsyncGenerator, Callable
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class KeyEvent(Enum):
|
|
10
|
+
UP = auto()
|
|
11
|
+
DOWN = auto()
|
|
12
|
+
LEFT = auto()
|
|
13
|
+
RIGHT = auto()
|
|
14
|
+
ENTER = auto()
|
|
15
|
+
ESCAPE = auto()
|
|
16
|
+
TAB = auto()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def listen_for_keyboard() -> AsyncGenerator[KeyEvent]:
|
|
20
|
+
loop = asyncio.get_running_loop()
|
|
21
|
+
queue = asyncio.Queue[KeyEvent]()
|
|
22
|
+
cancel_event = threading.Event()
|
|
23
|
+
|
|
24
|
+
def emit(event: KeyEvent) -> None:
|
|
25
|
+
# print(f"emit: {event}")
|
|
26
|
+
loop.call_soon_threadsafe(queue.put_nowait, event)
|
|
27
|
+
|
|
28
|
+
listener = threading.Thread(
|
|
29
|
+
target=_listen_for_keyboard_thread,
|
|
30
|
+
args=(cancel_event, emit),
|
|
31
|
+
name="kimi-cli-keyboard-listener",
|
|
32
|
+
daemon=True,
|
|
33
|
+
)
|
|
34
|
+
listener.start()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
while True:
|
|
38
|
+
yield await queue.get()
|
|
39
|
+
finally:
|
|
40
|
+
cancel_event.set()
|
|
41
|
+
if listener.is_alive():
|
|
42
|
+
await asyncio.to_thread(listener.join)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _listen_for_keyboard_thread(
|
|
46
|
+
cancel: threading.Event,
|
|
47
|
+
emit: Callable[[KeyEvent], None],
|
|
48
|
+
) -> None:
|
|
49
|
+
if sys.platform == "win32":
|
|
50
|
+
_listen_for_keyboard_windows(cancel, emit)
|
|
51
|
+
else:
|
|
52
|
+
_listen_for_keyboard_unix(cancel, emit)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _listen_for_keyboard_unix(
|
|
56
|
+
cancel: threading.Event,
|
|
57
|
+
emit: Callable[[KeyEvent], None],
|
|
58
|
+
) -> None:
|
|
59
|
+
if sys.platform == "win32":
|
|
60
|
+
raise RuntimeError("Unix keyboard listener requires a non-Windows platform")
|
|
61
|
+
|
|
62
|
+
import termios
|
|
63
|
+
|
|
64
|
+
# make stdin raw and non-blocking
|
|
65
|
+
fd = sys.stdin.fileno()
|
|
66
|
+
oldterm = termios.tcgetattr(fd)
|
|
67
|
+
newattr = termios.tcgetattr(fd)
|
|
68
|
+
newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
|
|
69
|
+
newattr[6][termios.VMIN] = 0
|
|
70
|
+
newattr[6][termios.VTIME] = 0
|
|
71
|
+
termios.tcsetattr(fd, termios.TCSANOW, newattr)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
while not cancel.is_set():
|
|
75
|
+
try:
|
|
76
|
+
c = sys.stdin.buffer.read(1)
|
|
77
|
+
except (OSError, ValueError):
|
|
78
|
+
c = b""
|
|
79
|
+
|
|
80
|
+
if not c:
|
|
81
|
+
if cancel.is_set():
|
|
82
|
+
break
|
|
83
|
+
time.sleep(0.01)
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if c == b"\x1b":
|
|
87
|
+
sequence = c
|
|
88
|
+
for _ in range(2):
|
|
89
|
+
if cancel.is_set():
|
|
90
|
+
break
|
|
91
|
+
try:
|
|
92
|
+
fragment = sys.stdin.buffer.read(1)
|
|
93
|
+
except (OSError, ValueError):
|
|
94
|
+
fragment = b""
|
|
95
|
+
if not fragment:
|
|
96
|
+
break
|
|
97
|
+
sequence += fragment
|
|
98
|
+
if sequence in _ARROW_KEY_MAP:
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
event = _ARROW_KEY_MAP.get(sequence)
|
|
102
|
+
if event is not None:
|
|
103
|
+
emit(event)
|
|
104
|
+
elif sequence == b"\x1b":
|
|
105
|
+
emit(KeyEvent.ESCAPE)
|
|
106
|
+
elif c in (b"\r", b"\n"):
|
|
107
|
+
emit(KeyEvent.ENTER)
|
|
108
|
+
elif c == b"\t":
|
|
109
|
+
emit(KeyEvent.TAB)
|
|
110
|
+
finally:
|
|
111
|
+
# restore the terminal settings
|
|
112
|
+
termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _listen_for_keyboard_windows(
|
|
116
|
+
cancel: threading.Event,
|
|
117
|
+
emit: Callable[[KeyEvent], None],
|
|
118
|
+
) -> None:
|
|
119
|
+
if sys.platform != "win32":
|
|
120
|
+
raise RuntimeError("Windows keyboard listener requires a Windows platform")
|
|
121
|
+
|
|
122
|
+
import msvcrt
|
|
123
|
+
|
|
124
|
+
while not cancel.is_set():
|
|
125
|
+
if msvcrt.kbhit():
|
|
126
|
+
c = msvcrt.getch()
|
|
127
|
+
|
|
128
|
+
# Handle special keys (arrow keys, etc.)
|
|
129
|
+
if c in (b"\x00", b"\xe0"):
|
|
130
|
+
# Extended key, read the next byte
|
|
131
|
+
extended = msvcrt.getch()
|
|
132
|
+
event = _WINDOWS_KEY_MAP.get(extended)
|
|
133
|
+
if event is not None:
|
|
134
|
+
emit(event)
|
|
135
|
+
elif c == b"\x1b":
|
|
136
|
+
sequence = c
|
|
137
|
+
for _ in range(2):
|
|
138
|
+
if cancel.is_set():
|
|
139
|
+
break
|
|
140
|
+
fragment = msvcrt.getch() if msvcrt.kbhit() else b""
|
|
141
|
+
if not fragment:
|
|
142
|
+
break
|
|
143
|
+
sequence += fragment
|
|
144
|
+
if sequence in _ARROW_KEY_MAP:
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
event = _ARROW_KEY_MAP.get(sequence)
|
|
148
|
+
if event is not None:
|
|
149
|
+
emit(event)
|
|
150
|
+
elif sequence == b"\x1b":
|
|
151
|
+
emit(KeyEvent.ESCAPE)
|
|
152
|
+
elif c in (b"\r", b"\n"):
|
|
153
|
+
emit(KeyEvent.ENTER)
|
|
154
|
+
elif c == b"\t":
|
|
155
|
+
emit(KeyEvent.TAB)
|
|
156
|
+
else:
|
|
157
|
+
if cancel.is_set():
|
|
158
|
+
break
|
|
159
|
+
time.sleep(0.01)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_ARROW_KEY_MAP: dict[bytes, KeyEvent] = {
|
|
163
|
+
b"\x1b[A": KeyEvent.UP,
|
|
164
|
+
b"\x1b[B": KeyEvent.DOWN,
|
|
165
|
+
b"\x1b[C": KeyEvent.RIGHT,
|
|
166
|
+
b"\x1b[D": KeyEvent.LEFT,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_WINDOWS_KEY_MAP: dict[bytes, KeyEvent] = {
|
|
170
|
+
b"H": KeyEvent.UP, # Up arrow
|
|
171
|
+
b"P": KeyEvent.DOWN, # Down arrow
|
|
172
|
+
b"M": KeyEvent.RIGHT, # Right arrow
|
|
173
|
+
b"K": KeyEvent.LEFT, # Left arrow
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
|
|
179
|
+
async def dev_main():
|
|
180
|
+
async for event in listen_for_keyboard():
|
|
181
|
+
print(event)
|
|
182
|
+
|
|
183
|
+
asyncio.run(dev_main())
|
kimi_cli/ui/shell/metacmd.py
CHANGED
|
@@ -2,19 +2,17 @@ import tempfile
|
|
|
2
2
|
import webbrowser
|
|
3
3
|
from collections.abc import Awaitable, Callable, Sequence
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from string import Template
|
|
6
5
|
from typing import TYPE_CHECKING, NamedTuple, overload
|
|
7
6
|
|
|
8
|
-
from kosong.
|
|
9
|
-
from kosong.base.message import ContentPart, Message, TextPart
|
|
7
|
+
from kosong.message import Message
|
|
10
8
|
from rich.panel import Panel
|
|
11
9
|
|
|
12
|
-
import kimi_cli.prompts
|
|
13
|
-
from kimi_cli.
|
|
14
|
-
from kimi_cli.soul import LLMNotSet
|
|
10
|
+
import kimi_cli.prompts as prompts
|
|
11
|
+
from kimi_cli.cli import Reload
|
|
15
12
|
from kimi_cli.soul.context import Context
|
|
16
13
|
from kimi_cli.soul.kimisoul import KimiSoul
|
|
17
14
|
from kimi_cli.soul.message import system
|
|
15
|
+
from kimi_cli.soul.runtime import load_agents_md
|
|
18
16
|
from kimi_cli.ui.shell.console import console
|
|
19
17
|
from kimi_cli.utils.changelog import CHANGELOG, format_release_notes
|
|
20
18
|
from kimi_cli.utils.logging import logger
|
|
@@ -23,6 +21,17 @@ if TYPE_CHECKING:
|
|
|
23
21
|
from kimi_cli.ui.shell import ShellApp
|
|
24
22
|
|
|
25
23
|
type MetaCmdFunc = Callable[["ShellApp", list[str]], None | Awaitable[None]]
|
|
24
|
+
"""
|
|
25
|
+
A function that runs as a meta command.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
LLMNotSet: When the LLM is not set.
|
|
29
|
+
ChatProviderError: When the LLM provider returns an error.
|
|
30
|
+
Reload: When the configuration should be reloaded.
|
|
31
|
+
asyncio.CancelledError: When the command is interrupted by user.
|
|
32
|
+
|
|
33
|
+
This is quite similar to the `Soul.run` method.
|
|
34
|
+
"""
|
|
26
35
|
|
|
27
36
|
|
|
28
37
|
class MetaCommand(NamedTuple):
|
|
@@ -165,15 +174,15 @@ def help(app: "ShellApp", args: list[str]):
|
|
|
165
174
|
@meta_command
|
|
166
175
|
def version(app: "ShellApp", args: list[str]):
|
|
167
176
|
"""Show version information"""
|
|
168
|
-
from kimi_cli import
|
|
177
|
+
from kimi_cli.constant import VERSION
|
|
169
178
|
|
|
170
|
-
console.print(f"kimi, version {
|
|
179
|
+
console.print(f"kimi, version {VERSION}")
|
|
171
180
|
|
|
172
181
|
|
|
173
182
|
@meta_command(name="release-notes")
|
|
174
183
|
def release_notes(app: "ShellApp", args: list[str]):
|
|
175
184
|
"""Show release notes"""
|
|
176
|
-
text = format_release_notes(CHANGELOG)
|
|
185
|
+
text = format_release_notes(CHANGELOG, include_lib_changes=False)
|
|
177
186
|
with console.pager(styles=True):
|
|
178
187
|
console.print(Panel.fit(text, border_style="wheat4", title="Release Notes"))
|
|
179
188
|
|
|
@@ -188,25 +197,18 @@ def feedback(app: "ShellApp", args: list[str]):
|
|
|
188
197
|
console.print(f"Please submit feedback at [underline]{ISSUE_URL}[/underline].")
|
|
189
198
|
|
|
190
199
|
|
|
191
|
-
@meta_command
|
|
200
|
+
@meta_command(kimi_soul_only=True)
|
|
192
201
|
async def init(app: "ShellApp", args: list[str]):
|
|
193
202
|
"""Analyze the codebase and generate an `AGENTS.md` file"""
|
|
194
|
-
|
|
195
|
-
if not isinstance(soul_bak, KimiSoul):
|
|
196
|
-
console.print("[red]Failed to analyze the codebase.[/red]")
|
|
197
|
-
return
|
|
203
|
+
assert isinstance(app.soul, KimiSoul)
|
|
198
204
|
|
|
205
|
+
soul_bak = app.soul
|
|
199
206
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
200
207
|
logger.info("Running `/init`")
|
|
201
208
|
console.print("Analyzing the codebase...")
|
|
202
209
|
tmp_context = Context(file_backend=Path(temp_dir) / "context.jsonl")
|
|
203
|
-
app.soul = KimiSoul(
|
|
204
|
-
|
|
205
|
-
soul_bak._agent_globals,
|
|
206
|
-
context=tmp_context,
|
|
207
|
-
loop_control=soul_bak._loop_control,
|
|
208
|
-
)
|
|
209
|
-
ok = await app._run(prompts.INIT)
|
|
210
|
+
app.soul = KimiSoul(soul_bak._agent, soul_bak._runtime, context=tmp_context)
|
|
211
|
+
ok = await app._run_soul_command(prompts.INIT, thinking=False)
|
|
210
212
|
|
|
211
213
|
if ok:
|
|
212
214
|
console.print(
|
|
@@ -217,7 +219,7 @@ async def init(app: "ShellApp", args: list[str]):
|
|
|
217
219
|
console.print("[red]Failed to analyze the codebase.[/red]")
|
|
218
220
|
|
|
219
221
|
app.soul = soul_bak
|
|
220
|
-
agents_md = load_agents_md(soul_bak.
|
|
222
|
+
agents_md = load_agents_md(soul_bak._runtime.builtin_args.KIMI_WORK_DIR)
|
|
221
223
|
system_message = system(
|
|
222
224
|
"The user just ran `/init` meta command. "
|
|
223
225
|
"The system has analyzed the codebase and generated an `AGENTS.md` file. "
|
|
@@ -232,78 +234,29 @@ async def clear(app: "ShellApp", args: list[str]):
|
|
|
232
234
|
assert isinstance(app.soul, KimiSoul)
|
|
233
235
|
|
|
234
236
|
if app.soul._context.n_checkpoints == 0:
|
|
235
|
-
|
|
236
|
-
return
|
|
237
|
+
raise Reload()
|
|
237
238
|
|
|
238
239
|
await app.soul._context.revert_to(0)
|
|
239
|
-
|
|
240
|
+
raise Reload()
|
|
240
241
|
|
|
241
242
|
|
|
242
|
-
@meta_command
|
|
243
|
+
@meta_command(kimi_soul_only=True)
|
|
243
244
|
async def compact(app: "ShellApp", args: list[str]):
|
|
244
245
|
"""Compact the context"""
|
|
245
246
|
assert isinstance(app.soul, KimiSoul)
|
|
246
247
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if app.soul._agent_globals.llm is None:
|
|
250
|
-
raise LLMNotSet()
|
|
251
|
-
|
|
252
|
-
# Get current context history
|
|
253
|
-
current_history = list(app.soul._context.history)
|
|
254
|
-
if len(current_history) <= 1:
|
|
255
|
-
console.print("[yellow]Context is too short to compact.[/yellow]")
|
|
248
|
+
if app.soul._context.n_checkpoints == 0:
|
|
249
|
+
console.print("[yellow]Context is empty.[/yellow]")
|
|
256
250
|
return
|
|
257
251
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
# Build the compact prompt using string template
|
|
265
|
-
compact_template = Template(prompts.COMPACT)
|
|
266
|
-
compact_prompt = compact_template.substitute(CONTEXT=history_text)
|
|
267
|
-
|
|
268
|
-
# Create input message for compaction
|
|
269
|
-
compact_message = Message(role="user", content=compact_prompt)
|
|
270
|
-
|
|
271
|
-
# Call generate to get the compacted context
|
|
272
|
-
try:
|
|
273
|
-
with console.status("[cyan]Compacting...[/cyan]"):
|
|
274
|
-
compacted_msg, usage = await generate(
|
|
275
|
-
chat_provider=app.soul._agent_globals.llm.chat_provider,
|
|
276
|
-
system_prompt="You are a helpful assistant that compacts conversation context.",
|
|
277
|
-
tools=[],
|
|
278
|
-
history=[compact_message],
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
# Clear the context and add the compacted message as the first message
|
|
282
|
-
await app.soul._context.revert_to(0)
|
|
283
|
-
content: list[ContentPart] = (
|
|
284
|
-
[TextPart(text=compacted_msg.content)]
|
|
285
|
-
if isinstance(compacted_msg.content, str)
|
|
286
|
-
else compacted_msg.content
|
|
287
|
-
)
|
|
288
|
-
content.insert(
|
|
289
|
-
0, system("Previous context has been compacted. Here is the compaction output:")
|
|
290
|
-
)
|
|
291
|
-
await app.soul._context.append_message(Message(role="assistant", content=content))
|
|
292
|
-
|
|
293
|
-
console.print("[green]✓[/green] Context has been compacted.")
|
|
294
|
-
if usage:
|
|
295
|
-
logger.info(
|
|
296
|
-
"Compaction used {input} input tokens and {output} output tokens",
|
|
297
|
-
input=usage.input,
|
|
298
|
-
output=usage.output,
|
|
299
|
-
)
|
|
300
|
-
except Exception as e:
|
|
301
|
-
logger.error("Failed to compact context: {error}", error=e)
|
|
302
|
-
console.print(f"[red]Failed to compact the context: {e}[/red]")
|
|
303
|
-
return
|
|
252
|
+
logger.info("Running `/compact`")
|
|
253
|
+
with console.status("[cyan]Compacting...[/cyan]"):
|
|
254
|
+
await app.soul.compact_context()
|
|
255
|
+
console.print("[green]✓[/green] Context has been compacted.")
|
|
304
256
|
|
|
305
257
|
|
|
306
258
|
from . import ( # noqa: E402
|
|
259
|
+
debug, # noqa: F401
|
|
307
260
|
setup, # noqa: F401
|
|
308
261
|
update, # noqa: F401
|
|
309
262
|
)
|