klaude-code 2.0.2__py3-none-any.whl → 2.1.1__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 (157) hide show
  1. klaude_code/app/__init__.py +12 -0
  2. klaude_code/app/runtime.py +215 -0
  3. klaude_code/cli/auth_cmd.py +2 -2
  4. klaude_code/cli/config_cmd.py +2 -2
  5. klaude_code/cli/cost_cmd.py +1 -1
  6. klaude_code/cli/debug.py +12 -36
  7. klaude_code/cli/list_model.py +3 -3
  8. klaude_code/cli/main.py +17 -60
  9. klaude_code/cli/self_update.py +2 -187
  10. klaude_code/cli/session_cmd.py +2 -2
  11. klaude_code/config/config.py +1 -1
  12. klaude_code/config/select_model.py +1 -1
  13. klaude_code/const.py +9 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +291 -0
  16. klaude_code/core/executor.py +335 -230
  17. klaude_code/core/manager/llm_clients_builder.py +1 -1
  18. klaude_code/core/manager/sub_agent_manager.py +16 -29
  19. klaude_code/core/reminders.py +84 -103
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -19
  22. klaude_code/core/tool/context.py +84 -0
  23. klaude_code/core/tool/file/apply_patch_tool.py +18 -21
  24. klaude_code/core/tool/file/edit_tool.py +39 -42
  25. klaude_code/core/tool/file/read_tool.py +14 -9
  26. klaude_code/core/tool/file/write_tool.py +12 -13
  27. klaude_code/core/tool/report_back_tool.py +4 -1
  28. klaude_code/core/tool/shell/bash_tool.py +6 -11
  29. klaude_code/core/tool/sub_agent_tool.py +8 -7
  30. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  31. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  32. klaude_code/core/tool/tool_abc.py +2 -1
  33. klaude_code/core/tool/tool_registry.py +2 -33
  34. klaude_code/core/tool/tool_runner.py +13 -10
  35. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  36. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  37. klaude_code/core/tool/web/web_search_tool.py +5 -3
  38. klaude_code/core/turn.py +87 -30
  39. klaude_code/llm/anthropic/client.py +1 -1
  40. klaude_code/llm/bedrock/client.py +1 -1
  41. klaude_code/llm/claude/client.py +1 -1
  42. klaude_code/llm/codex/client.py +1 -1
  43. klaude_code/llm/google/client.py +1 -1
  44. klaude_code/llm/openai_compatible/client.py +1 -1
  45. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  46. klaude_code/llm/openrouter/client.py +1 -1
  47. klaude_code/llm/openrouter/reasoning.py +1 -1
  48. klaude_code/llm/responses/client.py +1 -1
  49. klaude_code/protocol/commands.py +1 -0
  50. klaude_code/protocol/events/__init__.py +57 -0
  51. klaude_code/protocol/events/base.py +18 -0
  52. klaude_code/protocol/events/chat.py +20 -0
  53. klaude_code/protocol/events/lifecycle.py +22 -0
  54. klaude_code/protocol/events/metadata.py +15 -0
  55. klaude_code/protocol/events/streaming.py +43 -0
  56. klaude_code/protocol/events/system.py +53 -0
  57. klaude_code/protocol/events/tools.py +27 -0
  58. klaude_code/protocol/op.py +5 -0
  59. klaude_code/protocol/tools.py +0 -1
  60. klaude_code/session/session.py +6 -7
  61. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  62. klaude_code/skill/loader.py +32 -88
  63. klaude_code/skill/manager.py +38 -0
  64. klaude_code/skill/system_skills.py +1 -1
  65. klaude_code/tui/__init__.py +8 -0
  66. klaude_code/{command → tui/command}/__init__.py +3 -0
  67. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  68. klaude_code/tui/command/copy_cmd.py +53 -0
  69. klaude_code/{command → tui/command}/debug_cmd.py +3 -2
  70. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
  72. klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
  73. klaude_code/{command → tui/command}/help_cmd.py +2 -1
  74. klaude_code/{command → tui/command}/model_cmd.py +4 -3
  75. klaude_code/{command → tui/command}/model_select.py +2 -2
  76. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  77. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  78. klaude_code/{command → tui/command}/registry.py +6 -5
  79. klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
  80. klaude_code/{command → tui/command}/resume_cmd.py +4 -3
  81. klaude_code/{command → tui/command}/status_cmd.py +2 -1
  82. klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
  83. klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
  84. klaude_code/tui/commands.py +164 -0
  85. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  86. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  87. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  88. klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
  89. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  90. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  91. klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
  92. klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
  93. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  94. klaude_code/{ui → tui/components}/rich/theme.py +3 -1
  95. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  96. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  97. klaude_code/{ui/renderers → tui/components}/tools.py +13 -17
  98. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  99. klaude_code/tui/display.py +85 -0
  100. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  101. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  102. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
  103. klaude_code/tui/machine.py +608 -0
  104. klaude_code/tui/renderer.py +707 -0
  105. klaude_code/tui/runner.py +321 -0
  106. klaude_code/tui/terminal/__init__.py +56 -0
  107. klaude_code/{ui → tui}/terminal/color.py +1 -1
  108. klaude_code/{ui → tui}/terminal/control.py +1 -1
  109. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  110. klaude_code/ui/__init__.py +6 -50
  111. klaude_code/ui/core/display.py +3 -3
  112. klaude_code/ui/core/input.py +2 -1
  113. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  114. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
  115. klaude_code/ui/terminal/__init__.py +6 -54
  116. klaude_code/ui/terminal/title.py +31 -0
  117. klaude_code/update.py +163 -0
  118. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/METADATA +1 -1
  119. klaude_code-2.1.1.dist-info/RECORD +233 -0
  120. klaude_code/cli/runtime.py +0 -518
  121. klaude_code/core/prompt.py +0 -108
  122. klaude_code/core/tool/skill/skill_tool.md +0 -24
  123. klaude_code/core/tool/skill/skill_tool.py +0 -87
  124. klaude_code/core/tool/tool_context.py +0 -148
  125. klaude_code/protocol/events.py +0 -195
  126. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  127. klaude_code/trace/__init__.py +0 -21
  128. klaude_code/ui/core/stage_manager.py +0 -48
  129. klaude_code/ui/modes/__init__.py +0 -1
  130. klaude_code/ui/modes/debug/__init__.py +0 -1
  131. klaude_code/ui/modes/exec/__init__.py +0 -1
  132. klaude_code/ui/modes/repl/display.py +0 -61
  133. klaude_code/ui/modes/repl/event_handler.py +0 -629
  134. klaude_code/ui/modes/repl/renderer.py +0 -464
  135. klaude_code/ui/renderers/__init__.py +0 -0
  136. klaude_code/ui/utils/__init__.py +0 -1
  137. klaude_code-2.0.2.dist-info/RECORD +0 -227
  138. /klaude_code/{trace/log.py → log.py} +0 -0
  139. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  140. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  141. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  142. /klaude_code/{core/tool/skill → tui/components}/__init__.py +0 -0
  143. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  144. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  145. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  146. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  147. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  150. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  151. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  152. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  153. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  154. /klaude_code/{ui → tui}/terminal/selector.py +0 -0
  155. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  156. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/WHEEL +0 -0
  157. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/entry_points.txt +0 -0
@@ -289,7 +289,7 @@ class Session(BaseModel):
289
289
  return True
290
290
  return isinstance(prev_item, (message.UserMessage, message.ToolResultMessage, message.DeveloperMessage))
291
291
 
292
- def get_history_item(self) -> Iterable[events.HistoryItemEvent]:
292
+ def get_history_item(self) -> Iterable[events.ReplayEventUnion]:
293
293
  seen_sub_agent_sessions: set[str] = set()
294
294
  prev_item: message.HistoryEvent | None = None
295
295
  last_assistant_content: str = ""
@@ -314,8 +314,8 @@ class Session(BaseModel):
314
314
  response_id=am.response_id,
315
315
  session_id=self.id,
316
316
  )
317
- yield events.AssistantMessageEvent(
318
- thinking_text=thinking_text,
317
+ yield events.ResponseCompleteEvent(
318
+ thinking_text=thinking_text or None,
319
319
  content=content,
320
320
  response_id=am.response_id,
321
321
  session_id=self.id,
@@ -350,8 +350,6 @@ class Session(BaseModel):
350
350
  is_last_in_turn=is_last_in_turn,
351
351
  )
352
352
  yield from self._iter_sub_agent_history(tr, seen_sub_agent_sessions)
353
- if tr.status == "aborted":
354
- yield events.InterruptEvent(session_id=self.id)
355
353
  case message.UserMessage() as um:
356
354
  images = [part for part in um.parts if isinstance(part, message.ImageURLPart)]
357
355
  yield events.UserMessageEvent(
@@ -360,7 +358,8 @@ class Session(BaseModel):
360
358
  images=images or None,
361
359
  )
362
360
  case model.TaskMetadataItem() as mt:
363
- yield events.TaskMetadataEvent(session_id=self.id, metadata=mt)
361
+ if self.sub_agent_state is None:
362
+ yield events.TaskMetadataEvent(session_id=self.id, metadata=mt)
364
363
  case message.DeveloperMessage() as dm:
365
364
  yield events.DeveloperMessageEvent(session_id=self.id, item=dm)
366
365
  case message.StreamErrorItem() as se:
@@ -377,7 +376,7 @@ class Session(BaseModel):
377
376
 
378
377
  def _iter_sub_agent_history(
379
378
  self, tool_result: message.ToolResultMessage, seen_sub_agent_sessions: set[str]
380
- ) -> Iterable[events.HistoryItemEvent]:
379
+ ) -> Iterable[events.ReplayEventUnion]:
381
380
  ui_extra = tool_result.ui_extra
382
381
  if not isinstance(ui_extra, model.SessionIdUIExtra):
383
382
  return
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: create-plan
3
+ description: Create a concise plan. Use when a user explicitly asks for a plan related to a coding task.
4
+ metadata:
5
+ short-description: Create a plan
6
+ ---
7
+
8
+ # Create Plan
9
+
10
+ ## Goal
11
+
12
+ Turn a user prompt into a **single, actionable plan** delivered in the final assistant message.
13
+
14
+ ## Minimal workflow
15
+
16
+ 1. **Scan context quickly**
17
+ - Read `README.md` and any obvious docs (`docs/`, `CONTRIBUTING.md`, `ARCHITECTURE.md`).
18
+ - Skim relevant files (the ones most likely touched).
19
+ - Identify constraints (language, frameworks, CI/test commands, deployment shape).
20
+
21
+ 2. **Ask follow-ups only if blocking**
22
+ - Ask **at most 1–2 questions**.
23
+ - Only ask if you cannot responsibly plan without the answer; prefer multiple-choice.
24
+ - If unsure but not blocked, make a reasonable assumption and proceed.
25
+
26
+ 3. **Create a plan using the template below**
27
+ - Start with **1 short paragraph** describing the intent and approach.
28
+ - Clearly call out what is **in scope** and what is **not in scope** in short.
29
+ - Then provide a **small checklist** of action items (default 6–10 items).
30
+ - Each checklist item should be a concrete action and, when helpful, mention files/commands.
31
+ - **Make items atomic and ordered**: discovery → changes → tests → rollout.
32
+ - **Verb-first**: “Add…”, “Refactor…”, “Verify…”, “Ship…”.
33
+ - Include at least one item for **tests/validation** and one for **edge cases/risk** when applicable.
34
+ - If there are unknowns, include a tiny **Open questions** section (max 3).
35
+
36
+ 4. **Write the plan to `plan.md` in the current working directory**
37
+ - Use the Write tool to save the plan to `./plan.md`
38
+ - If `plan.md` already exists, overwrite it with the new plan
39
+
40
+ 5. **Do not preface the plan with meta explanations; output only the plan as per template**
41
+
42
+ ## Plan template (follow exactly)
43
+
44
+ ```markdown
45
+ # Plan
46
+
47
+ <1–3 sentences: what we’re doing, why, and the high-level approach.>
48
+
49
+ ## Scope
50
+ - In:
51
+ - Out:
52
+
53
+ ## Action items
54
+ [ ] <Step 1>
55
+ [ ] <Step 2>
56
+ [ ] <Step 3>
57
+ [ ] <Step 4>
58
+ [ ] <Step 5>
59
+ [ ] <Step 6>
60
+
61
+ ## Open questions
62
+ - <Question 1>
63
+ - <Question 2>
64
+ - <Question 3>
65
+ ```
66
+
67
+ ## Checklist item guidance
68
+ Good checklist items:
69
+ - Point to likely files/modules: src/..., app/..., services/...
70
+ - Name concrete validation: “Run npm test”, “Add unit tests for X”
71
+ - Include safe rollout when relevant: feature flag, migration plan, rollback note
72
+
73
+ Avoid:
74
+ - Vague steps (“handle backend”, “do auth”)
75
+ - Too many micro-steps
76
+ - Writing code snippets (keep the plan implementation-agnostic)
@@ -1,11 +1,10 @@
1
- import re
2
1
  from dataclasses import dataclass
3
2
  from pathlib import Path
4
3
  from typing import ClassVar
5
4
 
6
5
  import yaml
7
6
 
8
- from klaude_code.trace import log_debug
7
+ from klaude_code.log import log_debug
9
8
 
10
9
 
11
10
  @dataclass
@@ -14,12 +13,12 @@ class Skill:
14
13
 
15
14
  name: str # Skill identifier (lowercase-hyphen)
16
15
  description: str # What the skill does and when to use it
17
- content: str # Full markdown instructions
18
- location: str # Skill location: 'system', 'user', or 'project'
16
+ location: str # Skill source: 'system', 'user', or 'project'
17
+ skill_path: Path
18
+ base_dir: Path
19
19
  license: str | None = None
20
20
  allowed_tools: list[str] | None = None
21
21
  metadata: dict[str, str] | None = None
22
- skill_path: Path | None = None
23
22
 
24
23
  @property
25
24
  def short_description(self) -> str:
@@ -31,17 +30,6 @@ class Skill:
31
30
  return self.metadata["short-description"]
32
31
  return self.description
33
32
 
34
- def to_prompt(self) -> str:
35
- """Convert skill to prompt format for agent consumption"""
36
- return f"""# Skill: {self.name}
37
-
38
- {self.description}
39
-
40
- ---
41
-
42
- {self.content}
43
- """
44
-
45
33
 
46
34
  class SkillLoader:
47
35
  """Load and manage Claude Skills from SKILL.md files"""
@@ -79,7 +67,6 @@ class SkillLoader:
79
67
 
80
68
  # Parse YAML frontmatter
81
69
  frontmatter: dict[str, object] = {}
82
- markdown_content = content
83
70
 
84
71
  if content.startswith("---"):
85
72
  parts = content.split("---", 2)
@@ -87,7 +74,6 @@ class SkillLoader:
87
74
  loaded: object = yaml.safe_load(parts[1])
88
75
  if isinstance(loaded, dict):
89
76
  frontmatter = dict(loaded) # type: ignore[arg-type]
90
- markdown_content = parts[2].strip()
91
77
 
92
78
  # Extract skill metadata
93
79
  name = str(frontmatter.get("name", ""))
@@ -96,10 +82,6 @@ class SkillLoader:
96
82
  if not name or not description:
97
83
  return None
98
84
 
99
- # Process relative paths in content
100
- skill_dir = skill_path.parent
101
- processed_content = self._process_skill_paths(markdown_content, skill_dir)
102
-
103
85
  # Create Skill object
104
86
  license_val = frontmatter.get("license")
105
87
  allowed_tools_val = frontmatter.get("allowed-tools")
@@ -118,12 +100,12 @@ class SkillLoader:
118
100
  skill = Skill(
119
101
  name=name,
120
102
  description=description,
121
- content=processed_content,
122
103
  location=location,
123
104
  license=str(license_val) if license_val is not None else None,
124
105
  allowed_tools=allowed_tools,
125
106
  metadata=metadata,
126
- skill_path=skill_path,
107
+ skill_path=skill_path.resolve(),
108
+ base_dir=skill_path.parent.resolve(),
127
109
  )
128
110
 
129
111
  return skill
@@ -144,6 +126,15 @@ class SkillLoader:
144
126
  List of successfully loaded Skill objects
145
127
  """
146
128
  skills: list[Skill] = []
129
+ priority = {"system": 0, "user": 1, "project": 2}
130
+
131
+ def register(skill: Skill) -> None:
132
+ existing = self.loaded_skills.get(skill.name)
133
+ if existing is None:
134
+ self.loaded_skills[skill.name] = skill
135
+ return
136
+ if priority.get(skill.location, -1) >= priority.get(existing.location, -1):
137
+ self.loaded_skills[skill.name] = skill
147
138
 
148
139
  # Load system-level skills first (lowest priority, can be overridden)
149
140
  system_dir = self.SYSTEM_SKILLS_DIR.expanduser()
@@ -152,7 +143,7 @@ class SkillLoader:
152
143
  skill = self.load_skill(skill_file, location="system")
153
144
  if skill:
154
145
  skills.append(skill)
155
- self.loaded_skills[skill.name] = skill
146
+ register(skill)
156
147
 
157
148
  # Load user-level skills (override system skills if same name)
158
149
  for user_dir in self.USER_SKILLS_DIRS:
@@ -165,7 +156,7 @@ class SkillLoader:
165
156
  skill = self.load_skill(skill_file, location="user")
166
157
  if skill:
167
158
  skills.append(skill)
168
- self.loaded_skills[skill.name] = skill
159
+ register(skill)
169
160
 
170
161
  # Load project-level skills (override user skills if same name)
171
162
  project_dir = self.PROJECT_SKILLS_DIR.resolve()
@@ -174,13 +165,14 @@ class SkillLoader:
174
165
  skill = self.load_skill(skill_file, location="project")
175
166
  if skill:
176
167
  skills.append(skill)
177
- self.loaded_skills[skill.name] = skill
168
+ register(skill)
178
169
 
179
170
  # Log discovery summary
180
- if skills:
181
- system_count = sum(1 for s in skills if s.location == "system")
182
- user_count = sum(1 for s in skills if s.location == "user")
183
- project_count = sum(1 for s in skills if s.location == "project")
171
+ if self.loaded_skills:
172
+ selected = list(self.loaded_skills.values())
173
+ system_count = sum(1 for s in selected if s.location == "system")
174
+ user_count = sum(1 for s in selected if s.location == "user")
175
+ project_count = sum(1 for s in selected if s.location == "project")
184
176
  parts: list[str] = []
185
177
  if system_count > 0:
186
178
  parts.append(f"{system_count} system")
@@ -188,7 +180,7 @@ class SkillLoader:
188
180
  parts.append(f"{user_count} user")
189
181
  if project_count > 0:
190
182
  parts.append(f"{project_count} project")
191
- log_debug(f"Discovered {len(skills)} Claude Skills ({', '.join(parts)})")
183
+ log_debug(f"Loaded {len(self.loaded_skills)} Claude Skills ({', '.join(parts)})")
192
184
 
193
185
  return skills
194
186
 
@@ -224,62 +216,14 @@ class SkillLoader:
224
216
  XML string with all skill metadata
225
217
  """
226
218
  xml_parts: list[str] = []
227
- for skill in self.loaded_skills.values():
228
- xml_parts.append(f"""<skill>
219
+ # Prefer showing higher-priority skills first (project > user > system).
220
+ location_order = {"project": 0, "user": 1, "system": 2}
221
+ for skill in sorted(self.loaded_skills.values(), key=lambda s: location_order.get(s.location, 3)):
222
+ xml_parts.append(
223
+ f"""<skill>
229
224
  <name>{skill.name}</name>
230
225
  <description>{skill.description}</description>
231
- <location>{skill.location}</location>
232
- </skill>""")
226
+ <location>{skill.skill_path}</location>
227
+ </skill>"""
228
+ )
233
229
  return "\n".join(xml_parts)
234
-
235
- def _process_skill_paths(self, content: str, skill_dir: Path) -> str:
236
- """Convert relative paths to absolute paths for Level 3+
237
-
238
- Supports:
239
- - scripts/, examples/, templates/, reference/ directories
240
- - Markdown document references
241
- - Markdown links [text](path)
242
-
243
- Args:
244
- content: Original skill content
245
- skill_dir: Directory containing the SKILL.md file
246
-
247
- Returns:
248
- Content with absolute paths
249
- """
250
- # Pattern 1: Directory-based paths (scripts/, examples/, etc.)
251
- # e.g., "python scripts/generate.py" -> "python /abs/path/to/scripts/generate.py"
252
- dir_pattern = r"\b(scripts|examples|templates|reference)/([^\s\)]+)"
253
-
254
- def replace_dir_path(match: re.Match[str]) -> str:
255
- directory = match.group(1)
256
- filename = match.group(2)
257
- abs_path = skill_dir / directory / filename
258
- return str(abs_path)
259
-
260
- content = re.sub(dir_pattern, replace_dir_path, content)
261
-
262
- # Pattern 2: Markdown links [text](./path or path)
263
- # e.g., "[Guide](./docs/guide.md)" -> "[Guide](`/abs/path/to/docs/guide.md`) (use the Read tool to access)"
264
- link_pattern = r"\[([^\]]+)\]\((\./)?([^\)]+\.md)\)"
265
-
266
- def replace_link(match: re.Match[str]) -> str:
267
- text = match.group(1)
268
- filename = match.group(3)
269
- abs_path = skill_dir / filename
270
- return f"[{text}](`{abs_path}`) (use the Read tool to access)"
271
-
272
- content = re.sub(link_pattern, replace_link, content)
273
-
274
- # Pattern 3: Standalone markdown references
275
- # e.g., "see reference.md" -> "see `/abs/path/to/reference.md` (use the Read tool to access)"
276
- standalone_pattern = r"(?<!\])\b(\w+\.md)\b(?!\))"
277
-
278
- def replace_standalone(match: re.Match[str]) -> str:
279
- filename = match.group(1)
280
- abs_path = skill_dir / filename
281
- return f"`{abs_path}` (use the Read tool to access)"
282
-
283
- content = re.sub(standalone_pattern, replace_standalone, content)
284
-
285
- return content
@@ -68,3 +68,41 @@ def list_skill_names() -> list[str]:
68
68
  List of skill names
69
69
  """
70
70
  return _ensure_initialized().list_skills()
71
+
72
+
73
+ def format_available_skills_for_system_prompt() -> str:
74
+ """Format skills metadata for inclusion in the system prompt.
75
+
76
+ This follows the progressive-disclosure approach:
77
+ - Keep only name/description + file location in the always-on system prompt
78
+ - Load the full SKILL.md content on demand via the Read tool when needed
79
+ """
80
+
81
+ try:
82
+ loader = _ensure_initialized()
83
+ skills_xml = loader.get_skills_xml().strip()
84
+ if not skills_xml:
85
+ return ""
86
+
87
+ return f"""\
88
+ # Skills
89
+
90
+ Skills are optional task-specific instructions stored as `SKILL.md` files.
91
+
92
+ How to use skills:
93
+ - Use the metadata in <available_skills> to decide whether a skill applies.
94
+ - When the task matches a skill's description, use the `Read` tool to load the `SKILL.md` at the given <location>.
95
+ - If the user explicitly activates a skill by starting their message with `$skill-name`, prioritize that skill.
96
+
97
+ Important:
98
+ - Only use skills listed in <available_skills> below.
99
+ - Keep context small: do NOT load skill files unless needed.
100
+
101
+ The list below is metadata only (name/description/location). The full instructions live in the referenced file.
102
+
103
+ <available_skills>
104
+ {skills_xml}
105
+ </available_skills>"""
106
+ except Exception:
107
+ # Skills are an optional enhancement; do not fail prompt construction if discovery breaks.
108
+ return ""
@@ -11,7 +11,7 @@ from contextlib import contextmanager
11
11
  from importlib import resources
12
12
  from pathlib import Path
13
13
 
14
- from klaude_code.trace import log_debug
14
+ from klaude_code.log import log_debug
15
15
 
16
16
  # Marker file name for tracking installed skills version
17
17
  SYSTEM_SKILLS_MARKER_FILENAME = ".klaude-system-skills.marker"
@@ -0,0 +1,8 @@
1
+ """Terminal (TUI) frontend for klaude-code.
2
+
3
+ This package contains all terminal-specific UI code (Rich rendering,
4
+ prompt-toolkit input, and terminal integrations).
5
+
6
+ The tui layer may depend on `klaude_code.ui`, but `klaude_code.ui` must not
7
+ depend on `klaude_code.tui`.
8
+ """
@@ -30,6 +30,7 @@ def ensure_commands_loaded() -> None:
30
30
 
31
31
  # Import and register commands in display order
32
32
  from .clear_cmd import ClearCommand
33
+ from .copy_cmd import CopyCommand
33
34
  from .debug_cmd import DebugCommand
34
35
  from .export_cmd import ExportCommand
35
36
  from .export_online_cmd import ExportOnlineCommand
@@ -49,6 +50,7 @@ def ensure_commands_loaded() -> None:
49
50
  register(RefreshTerminalCommand())
50
51
  register(ThinkingCommand())
51
52
  register(ModelCommand())
53
+ register(CopyCommand())
52
54
  register(ForkSessionCommand())
53
55
  register(ResumeCommand())
54
56
  load_prompt_commands()
@@ -66,6 +68,7 @@ def ensure_commands_loaded() -> None:
66
68
  def __getattr__(name: str) -> object:
67
69
  _commands_map = {
68
70
  "ClearCommand": "clear_cmd",
71
+ "CopyCommand": "copy_cmd",
69
72
  "DebugCommand": "debug_cmd",
70
73
  "ExportCommand": "export_cmd",
71
74
  "ExportOnlineCommand": "export_online_cmd",
@@ -1,6 +1,7 @@
1
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
2
1
  from klaude_code.protocol import commands, message, op
3
2
 
3
+ from .command_abc import Agent, CommandABC, CommandResult
4
+
4
5
 
5
6
  class ClearCommand(CommandABC):
6
7
  """Clear current session and start a new conversation"""
@@ -0,0 +1,53 @@
1
+ from klaude_code.protocol import commands, events, message, model
2
+ from klaude_code.tui.input.clipboard import copy_to_clipboard
3
+
4
+ from .command_abc import Agent, CommandABC, CommandResult
5
+
6
+
7
+ class CopyCommand(CommandABC):
8
+ """Copy the last assistant message to system clipboard."""
9
+
10
+ @property
11
+ def name(self) -> commands.CommandName:
12
+ return commands.CommandName.COPY
13
+
14
+ @property
15
+ def summary(self) -> str:
16
+ return "Copy last assistant message to clipboard"
17
+
18
+ async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
19
+ del user_input # unused
20
+
21
+ last = _get_last_assistant_text(agent.session.conversation_history)
22
+ if not last:
23
+ return _developer_message(agent, "(no assistant message to copy)", self.name)
24
+
25
+ copy_to_clipboard(last)
26
+ return _developer_message(agent, "Copied last assistant message to clipboard.", self.name)
27
+
28
+
29
+ def _get_last_assistant_text(history: list[message.HistoryEvent]) -> str:
30
+ for item in reversed(history):
31
+ if not isinstance(item, message.AssistantMessage):
32
+ continue
33
+ content = message.join_text_parts(item.parts)
34
+ images = [part for part in item.parts if isinstance(part, message.ImageFilePart)]
35
+ formatted = message.format_saved_images(images, content)
36
+ return formatted.strip()
37
+ return ""
38
+
39
+
40
+ def _developer_message(agent: Agent, content: str, command_name: commands.CommandName) -> CommandResult:
41
+ return CommandResult(
42
+ events=[
43
+ events.DeveloperMessageEvent(
44
+ session_id=agent.session.id,
45
+ item=message.DeveloperMessage(
46
+ parts=message.text_parts_from_str(content),
47
+ ui_extra=model.build_command_output_extra(command_name),
48
+ ),
49
+ )
50
+ ],
51
+ persist_user_input=False,
52
+ persist_events=False,
53
+ )
@@ -1,6 +1,7 @@
1
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
1
+ from klaude_code.log import DebugType, get_current_log_file, is_debug_enabled, set_debug_logging
2
2
  from klaude_code.protocol import commands, events, message, model
3
- from klaude_code.trace import DebugType, get_current_log_file, is_debug_enabled, set_debug_logging
3
+
4
+ from .command_abc import Agent, CommandABC, CommandResult
4
5
 
5
6
 
6
7
  def _format_status() -> str:
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
5
  from klaude_code.protocol import commands, message, op
7
6
 
7
+ from .command_abc import Agent, CommandABC, CommandResult
8
+
8
9
 
9
10
  class ExportCommand(CommandABC):
10
11
  """Export the current session into a standalone HTML transcript."""
@@ -9,10 +9,11 @@ from pathlib import Path
9
9
  from rich.console import Console
10
10
  from rich.text import Text
11
11
 
12
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
13
12
  from klaude_code.protocol import commands, events, message, model
14
13
  from klaude_code.session.export import build_export_html
15
14
 
15
+ from .command_abc import Agent, CommandABC, CommandResult
16
+
16
17
 
17
18
  class ExportOnlineCommand(CommandABC):
18
19
  """Export and deploy the current session to surge.sh as a static webpage."""
@@ -5,10 +5,11 @@ from typing import Literal
5
5
 
6
6
  from prompt_toolkit.styles import Style
7
7
 
8
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
9
8
  from klaude_code.protocol import commands, events, message, model
10
- from klaude_code.ui.modes.repl.clipboard import copy_to_clipboard
11
- from klaude_code.ui.terminal.selector import SelectItem, select_one
9
+ from klaude_code.tui.input.clipboard import copy_to_clipboard
10
+ from klaude_code.tui.terminal.selector import SelectItem, select_one
11
+
12
+ from .command_abc import Agent, CommandABC, CommandResult
12
13
 
13
14
  FORK_SELECT_STYLE = Style(
14
15
  [
@@ -1,6 +1,7 @@
1
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
2
1
  from klaude_code.protocol import commands, events, message, model
3
2
 
3
+ from .command_abc import Agent, CommandABC, CommandResult
4
+
4
5
 
5
6
  class HelpCommand(CommandABC):
6
7
  """Display help information for all available slash commands."""
@@ -2,10 +2,11 @@ import asyncio
2
2
 
3
3
  from prompt_toolkit.styles import Style
4
4
 
5
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
- from klaude_code.command.model_select import select_model_interactive
7
5
  from klaude_code.protocol import commands, events, message, model, op
8
- from klaude_code.ui.terminal.selector import SelectItem, select_one
6
+ from klaude_code.tui.terminal.selector import SelectItem, select_one
7
+
8
+ from .command_abc import Agent, CommandABC, CommandResult
9
+ from .model_select import select_model_interactive
9
10
 
10
11
  SELECT_STYLE = Style(
11
12
  [
@@ -4,7 +4,7 @@ import sys
4
4
 
5
5
  from klaude_code.config.config import load_config
6
6
  from klaude_code.config.select_model import match_model_from_config
7
- from klaude_code.trace import log
7
+ from klaude_code.log import log
8
8
 
9
9
 
10
10
  def select_model_interactive(preferred: str | None = None) -> str | None:
@@ -38,7 +38,7 @@ def select_model_interactive(preferred: str | None = None) -> str | None:
38
38
  # Interactive selection
39
39
  from prompt_toolkit.styles import Style
40
40
 
41
- from klaude_code.ui.terminal.selector import build_model_select_items, select_one
41
+ from klaude_code.tui.terminal.selector import build_model_select_items, select_one
42
42
 
43
43
  config = load_config()
44
44
  names = [m.model_name for m in result.filtered_models]
@@ -2,9 +2,10 @@ from importlib.resources import files
2
2
 
3
3
  import yaml
4
4
 
5
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
5
+ from klaude_code.log import log_debug
6
6
  from klaude_code.protocol import commands, message, op
7
- from klaude_code.trace import log_debug
7
+
8
+ from .command_abc import Agent, CommandABC, CommandResult
8
9
 
9
10
 
10
11
  class PromptCommand(CommandABC):
@@ -30,7 +31,7 @@ class PromptCommand(CommandABC):
30
31
  return
31
32
 
32
33
  try:
33
- raw_text = files("klaude_code.command").joinpath(self.template_name).read_text(encoding="utf-8")
34
+ raw_text = files("klaude_code.tui.command").joinpath(self.template_name).read_text(encoding="utf-8")
34
35
 
35
36
  if raw_text.startswith("---"):
36
37
  parts = raw_text.split("---", 2)
@@ -1,6 +1,7 @@
1
- from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
2
1
  from klaude_code.protocol import commands, events, message
3
2
 
3
+ from .command_abc import Agent, CommandABC, CommandResult
4
+
4
5
 
5
6
  class RefreshTerminalCommand(CommandABC):
6
7
  """Refresh terminal display"""
@@ -26,6 +27,7 @@ class RefreshTerminalCommand(CommandABC):
26
27
  return CommandResult(
27
28
  events=[
28
29
  events.WelcomeEvent(
30
+ session_id=agent.session.id,
29
31
  work_dir=str(agent.session.work_dir),
30
32
  llm_config=agent.get_llm_client().get_llm_config(),
31
33
  ),
@@ -1,10 +1,11 @@
1
1
  from importlib.resources import files
2
2
  from typing import TYPE_CHECKING
3
3
 
4
- from klaude_code.command.command_abc import Agent, CommandResult
5
- from klaude_code.command.prompt_command import PromptCommand
4
+ from klaude_code.log import log_debug
6
5
  from klaude_code.protocol import commands, events, message, model, op
7
- from klaude_code.trace import log_debug
6
+
7
+ from .command_abc import Agent, CommandResult
8
+ from .prompt_command import PromptCommand
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from .command_abc import CommandABC
@@ -82,7 +83,7 @@ def register(cmd: "CommandABC") -> None:
82
83
  def load_prompt_commands():
83
84
  """Dynamically load prompt-based commands from the command directory."""
84
85
  try:
85
- command_files = files("klaude_code.command").iterdir()
86
+ command_files = files("klaude_code.tui.command").iterdir()
86
87
  for file_path in command_files:
87
88
  name = file_path.name
88
89
  if (name.startswith("prompt_") or name.startswith("prompt-")) and name.endswith(".md"):
@@ -94,7 +95,7 @@ def load_prompt_commands():
94
95
 
95
96
  def _ensure_commands_loaded() -> None:
96
97
  """Ensure all commands are loaded (lazy initialization)."""
97
- from klaude_code.command import ensure_commands_loaded
98
+ from klaude_code.tui.command import ensure_commands_loaded
98
99
 
99
100
  ensure_commands_loaded()
100
101