klaude-code 1.2.6__py3-none-any.whl → 1.8.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 (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -1,17 +1,29 @@
1
- import json
1
+ import hashlib
2
+ import re
3
+ import shlex
4
+ from collections.abc import Awaitable, Callable
5
+ from dataclasses import dataclass
2
6
  from pathlib import Path
3
- from typing import Awaitable, Callable
4
7
 
5
8
  from pydantic import BaseModel
6
9
 
7
10
  from klaude_code import const
8
11
  from klaude_code.core.tool import BashTool, ReadTool, reset_tool_context, set_tool_context_from_session
12
+ from klaude_code.core.tool.file._utils import hash_text_sha256
9
13
  from klaude_code.protocol import model, tools
10
14
  from klaude_code.session import Session
15
+ from klaude_code.skill import get_skill
11
16
 
12
17
  type Reminder = Callable[[Session], Awaitable[model.DeveloperMessageItem | None]]
13
18
 
14
19
 
20
+ # Match @ preceded by whitespace, start of line, or → (ReadTool line number arrow)
21
+ AT_FILE_PATTERN = re.compile(r'(?:(?<!\S)|(?<=\u2192))@("(?P<quoted>[^\"]+)"|(?P<plain>\S+))')
22
+
23
+ # Match $skill or ¥skill at the beginning of the first line
24
+ SKILL_PATTERN = re.compile(r"^[$¥](?P<skill>\S+)")
25
+
26
+
15
27
  def get_last_new_user_input(session: Session) -> str | None:
16
28
  """Get last user input & developer message (CLAUDE.md) from conversation history. if there's a tool result after user input, return None"""
17
29
  result: list[str] = []
@@ -26,56 +38,165 @@ def get_last_new_user_input(session: Session) -> str | None:
26
38
  return "\n\n".join(result)
27
39
 
28
40
 
29
- async def at_file_reader_reminder(
30
- session: Session,
31
- ) -> model.DeveloperMessageItem | None:
32
- """Parse @foo/bar to read"""
33
- last_user_input = get_last_new_user_input(session)
34
- if not last_user_input or "@" not in last_user_input.strip():
35
- return None
41
+ @dataclass
42
+ class AtPatternSource:
43
+ """Represents an @ pattern with its source file (if from a memory file)."""
44
+
45
+ pattern: str
46
+ mentioned_in: str | None = None
47
+
48
+
49
+ def _extract_at_patterns(content: str) -> list[str]:
50
+ """Extract @ patterns from content."""
51
+ patterns: list[str] = []
52
+ if "@" in content:
53
+ for match in AT_FILE_PATTERN.finditer(content):
54
+ path_str = match.group("quoted") or match.group("plain")
55
+ if path_str:
56
+ patterns.append(path_str)
57
+ return patterns
58
+
59
+
60
+ def get_at_patterns_with_source(session: Session) -> list[AtPatternSource]:
61
+ """Get @ patterns from last user input and developer messages, preserving source info."""
62
+ patterns: list[AtPatternSource] = []
63
+
64
+ for item in reversed(session.conversation_history):
65
+ if isinstance(item, model.ToolResultItem):
66
+ break
67
+
68
+ if isinstance(item, model.UserMessageItem):
69
+ content = item.content or ""
70
+ for path_str in _extract_at_patterns(content):
71
+ patterns.append(AtPatternSource(pattern=path_str, mentioned_in=None))
72
+ break
73
+
74
+ if isinstance(item, model.DeveloperMessageItem) and item.memory_mentioned:
75
+ for memory_path, mentioned_patterns in item.memory_mentioned.items():
76
+ for pattern in mentioned_patterns:
77
+ patterns.append(AtPatternSource(pattern=pattern, mentioned_in=memory_path))
78
+ return patterns
36
79
 
37
- at_patterns: list[str] = []
38
80
 
39
- for item in last_user_input.strip().split():
40
- if item.startswith("@") and len(item) > 1:
41
- at_patterns.append(item.lower().strip("@"))
81
+ def get_skill_from_user_input(session: Session) -> str | None:
82
+ """Get $skill reference from the first line of last user input."""
83
+ for item in reversed(session.conversation_history):
84
+ if isinstance(item, model.ToolResultItem):
85
+ return None
86
+ if isinstance(item, model.UserMessageItem):
87
+ content = item.content or ""
88
+ first_line = content.split("\n", 1)[0]
89
+ m = SKILL_PATTERN.match(first_line)
90
+ if m:
91
+ return m.group("skill")
92
+ return None
93
+ return None
94
+
95
+
96
+ def _is_tracked_file_unchanged(session: Session, path: str) -> bool:
97
+ status = session.file_tracker.get(path)
98
+ if status is None or status.content_sha256 is None:
99
+ return False
100
+
101
+ try:
102
+ current_mtime = Path(path).stat().st_mtime
103
+ except (OSError, FileNotFoundError):
104
+ return False
105
+
106
+ if current_mtime == status.mtime:
107
+ return True
42
108
 
43
- if len(at_patterns) == 0:
109
+ current_sha256 = _compute_file_content_sha256(path)
110
+ return current_sha256 is not None and current_sha256 == status.content_sha256
111
+
112
+
113
+ async def _load_at_file_recursive(
114
+ session: Session,
115
+ pattern: str,
116
+ at_files: dict[str, model.AtPatternParseResult],
117
+ collected_images: list[model.ImageURLPart],
118
+ visited: set[str],
119
+ base_dir: Path | None = None,
120
+ mentioned_in: str | None = None,
121
+ ) -> None:
122
+ """Recursively load @ file references."""
123
+ path = (base_dir / pattern).resolve() if base_dir else Path(pattern).resolve()
124
+ path_str = str(path)
125
+
126
+ if path_str in visited:
127
+ return
128
+ visited.add(path_str)
129
+
130
+ context_token = set_tool_context_from_session(session)
131
+ try:
132
+ if path.exists() and path.is_file():
133
+ if _is_tracked_file_unchanged(session, path_str):
134
+ return
135
+ args = ReadTool.ReadArguments(file_path=path_str)
136
+ tool_result = await ReadTool.call_with_args(args)
137
+ at_files[path_str] = model.AtPatternParseResult(
138
+ path=path_str,
139
+ tool_name=tools.READ,
140
+ result=tool_result.output or "",
141
+ tool_args=args.model_dump_json(exclude_none=True),
142
+ operation="Read",
143
+ images=tool_result.images,
144
+ mentioned_in=mentioned_in,
145
+ )
146
+ if tool_result.images:
147
+ collected_images.extend(tool_result.images)
148
+
149
+ # Recursively parse @ references from ReadTool output
150
+ output = tool_result.output or ""
151
+ if "@" in output:
152
+ for match in AT_FILE_PATTERN.finditer(output):
153
+ nested = match.group("quoted") or match.group("plain")
154
+ if nested:
155
+ await _load_at_file_recursive(
156
+ session,
157
+ nested,
158
+ at_files,
159
+ collected_images,
160
+ visited,
161
+ base_dir=path.parent,
162
+ mentioned_in=path_str,
163
+ )
164
+ elif path.exists() and path.is_dir():
165
+ quoted_path = shlex.quote(path_str)
166
+ args = BashTool.BashArguments(command=f"ls {quoted_path}")
167
+ tool_result = await BashTool.call_with_args(args)
168
+ at_files[path_str] = model.AtPatternParseResult(
169
+ path=path_str + "/",
170
+ tool_name=tools.BASH,
171
+ result=tool_result.output or "",
172
+ tool_args=args.model_dump_json(exclude_none=True),
173
+ operation="List",
174
+ )
175
+ finally:
176
+ reset_tool_context(context_token)
177
+
178
+
179
+ async def at_file_reader_reminder(
180
+ session: Session,
181
+ ) -> model.DeveloperMessageItem | None:
182
+ """Parse @foo/bar to read, with recursive loading of nested @ references"""
183
+ at_pattern_sources = get_at_patterns_with_source(session)
184
+ if not at_pattern_sources:
44
185
  return None
45
186
 
46
187
  at_files: dict[str, model.AtPatternParseResult] = {} # path -> content
47
188
  collected_images: list[model.ImageURLPart] = []
48
-
49
- for pattern in at_patterns:
50
- path = Path(pattern).resolve()
51
- context_token = set_tool_context_from_session(session)
52
- try:
53
- if path.exists() and path.is_file():
54
- args = ReadTool.ReadArguments(file_path=str(path))
55
- tool_result = await ReadTool.call_with_args(args)
56
- at_result = model.AtPatternParseResult(
57
- path=str(path),
58
- tool_name=tools.READ,
59
- result=tool_result.output or "",
60
- tool_args=args.model_dump_json(exclude_none=True),
61
- operation="Read",
62
- images=tool_result.images,
63
- )
64
- at_files[str(path)] = at_result
65
- if tool_result.images:
66
- collected_images.extend(tool_result.images)
67
- elif path.exists() and path.is_dir():
68
- args = BashTool.BashArguments(command=f"ls {path}")
69
- tool_result = await BashTool.call_with_args(args)
70
- at_files[str(path)] = model.AtPatternParseResult(
71
- path=str(path) + "/",
72
- tool_name=tools.BASH,
73
- result=tool_result.output or "",
74
- tool_args=args.model_dump_json(exclude_none=True),
75
- operation="List",
76
- )
77
- finally:
78
- reset_tool_context(context_token)
189
+ visited: set[str] = set()
190
+
191
+ for source in at_pattern_sources:
192
+ await _load_at_file_recursive(
193
+ session,
194
+ source.pattern,
195
+ at_files,
196
+ collected_images,
197
+ visited,
198
+ mentioned_in=source.mentioned_in,
199
+ )
79
200
 
80
201
  if len(at_files) == 0:
81
202
  return None
@@ -141,16 +262,16 @@ async def todo_not_used_recently_reminder(
141
262
  return None
142
263
 
143
264
  # Count non-todo tool calls since the last TodoWrite
144
- other_tool_call_count_befor_last_todo = 0
265
+ other_tool_call_count_before_last_todo = 0
145
266
  for item in reversed(session.conversation_history):
146
267
  if isinstance(item, model.ToolCallItem):
147
268
  if item.name in (tools.TODO_WRITE, tools.UPDATE_PLAN):
148
269
  break
149
- other_tool_call_count_befor_last_todo += 1
150
- if other_tool_call_count_befor_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD:
270
+ other_tool_call_count_before_last_todo += 1
271
+ if other_tool_call_count_before_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD:
151
272
  break
152
273
 
153
- not_used_recently = other_tool_call_count_befor_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD
274
+ not_used_recently = other_tool_call_count_before_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD
154
275
 
155
276
  if not not_used_recently:
156
277
  return None
@@ -180,9 +301,19 @@ async def file_changed_externally_reminder(
180
301
  changed_files: list[tuple[str, str, list[model.ImageURLPart] | None]] = []
181
302
  collected_images: list[model.ImageURLPart] = []
182
303
  if session.file_tracker and len(session.file_tracker) > 0:
183
- for path, mtime in session.file_tracker.items():
304
+ for path, status in session.file_tracker.items():
184
305
  try:
185
- if Path(path).stat().st_mtime > mtime:
306
+ current_mtime = Path(path).stat().st_mtime
307
+
308
+ changed = False
309
+ if status.content_sha256 is not None:
310
+ current_sha256 = _compute_file_content_sha256(path)
311
+ changed = current_sha256 is not None and current_sha256 != status.content_sha256
312
+ else:
313
+ # Backward-compat: old sessions only tracked mtime.
314
+ changed = current_mtime != status.mtime
315
+
316
+ if changed:
186
317
  context_token = set_tool_context_from_session(session)
187
318
  try:
188
319
  tool_result = await ReadTool.call_with_args(
@@ -219,6 +350,24 @@ async def file_changed_externally_reminder(
219
350
  return None
220
351
 
221
352
 
353
+ def _compute_file_content_sha256(path: str) -> str | None:
354
+ """Compute SHA-256 for file content using the same decoding behavior as ReadTool."""
355
+
356
+ try:
357
+ suffix = Path(path).suffix.lower()
358
+ if suffix in {".png", ".jpg", ".jpeg", ".gif", ".webp"}:
359
+ with open(path, "rb") as f:
360
+ return hashlib.sha256(f.read()).hexdigest()
361
+
362
+ hasher = hashlib.sha256()
363
+ with open(path, encoding="utf-8", errors="replace") as f:
364
+ for line in f:
365
+ hasher.update(line.encode("utf-8"))
366
+ return hasher.hexdigest()
367
+ except (FileNotFoundError, IsADirectoryError, OSError, PermissionError, UnicodeDecodeError):
368
+ return None
369
+
370
+
222
371
  def get_memory_paths() -> list[tuple[Path, str]]:
223
372
  return [
224
373
  (
@@ -230,8 +379,8 @@ def get_memory_paths() -> list[tuple[Path, str]]:
230
379
  "user's private global instructions for all projects",
231
380
  ),
232
381
  (Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
233
- (Path.cwd() / "AGENT.md", "project instructions, checked into the codebase"),
234
382
  (Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
383
+ (Path.cwd() / ".claude" / "CLAUDE.md", "project instructions, checked into the codebase"),
235
384
  ]
236
385
 
237
386
 
@@ -263,22 +412,79 @@ async def image_reminder(session: Session) -> model.DeveloperMessageItem | None:
263
412
  )
264
413
 
265
414
 
415
+ async def skill_reminder(session: Session) -> model.DeveloperMessageItem | None:
416
+ """Load skill content when user references a skill with $skill syntax."""
417
+ skill_name = get_skill_from_user_input(session)
418
+ if not skill_name:
419
+ return None
420
+
421
+ # Get the skill from skill module
422
+ skill = get_skill(skill_name)
423
+ if not skill:
424
+ return None
425
+
426
+ # Get base directory from skill_path
427
+ base_dir = str(skill.skill_path.parent) if skill.skill_path else "unknown"
428
+
429
+ content = f"""<system-reminder>The user activated the "{skill.name}" skill. Here is the skill content:
430
+
431
+ <skill>
432
+ <name>{skill.name}</name>
433
+ <base_dir>{base_dir}</base_dir>
434
+
435
+ {skill.to_prompt()}
436
+ </skill>
437
+ </system-reminder>"""
438
+
439
+ return model.DeveloperMessageItem(
440
+ content=content,
441
+ skill_name=skill.name,
442
+ )
443
+
444
+
445
+ def _is_memory_loaded(session: Session, path: str) -> bool:
446
+ """Check if a memory file has already been loaded (tracked with is_memory=True)."""
447
+ status = session.file_tracker.get(path)
448
+ return status is not None and status.is_memory
449
+
450
+
451
+ def _mark_memory_loaded(session: Session, path: str) -> None:
452
+ """Mark a file as loaded memory in file_tracker."""
453
+ try:
454
+ mtime = Path(path).stat().st_mtime
455
+ except (OSError, FileNotFoundError):
456
+ mtime = 0.0
457
+ try:
458
+ content_sha256 = hash_text_sha256(Path(path).read_text(encoding="utf-8", errors="replace"))
459
+ except (OSError, FileNotFoundError, PermissionError, UnicodeDecodeError):
460
+ content_sha256 = None
461
+ session.file_tracker[path] = model.FileStatus(mtime=mtime, content_sha256=content_sha256, is_memory=True)
462
+
463
+
266
464
  async def memory_reminder(session: Session) -> model.DeveloperMessageItem | None:
267
465
  """CLAUDE.md AGENTS.md"""
268
466
  memory_paths = get_memory_paths()
269
467
  memories: list[Memory] = []
270
468
  for memory_path, instruction in memory_paths:
271
- if memory_path.exists() and memory_path.is_file() and str(memory_path) not in session.loaded_memory:
469
+ path_str = str(memory_path)
470
+ if memory_path.exists() and memory_path.is_file() and not _is_memory_loaded(session, path_str):
272
471
  try:
273
472
  text = memory_path.read_text()
274
- session.loaded_memory.append(str(memory_path))
275
- memories.append(Memory(path=str(memory_path), instruction=instruction, content=text))
473
+ _mark_memory_loaded(session, path_str)
474
+ memories.append(Memory(path=path_str, instruction=instruction, content=text))
276
475
  except (PermissionError, UnicodeDecodeError, OSError):
277
476
  continue
278
477
  if len(memories) > 0:
279
478
  memories_str = "\n\n".join(
280
479
  [f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
281
480
  )
481
+ # Build memory_mentioned: extract @ patterns from each memory's content
482
+ memory_mentioned: dict[str, list[str]] = {}
483
+ for memory in memories:
484
+ patterns = _extract_at_patterns(memory.content)
485
+ if patterns:
486
+ memory_mentioned[memory.path] = patterns
487
+
282
488
  return model.DeveloperMessageItem(
283
489
  content=f"""<system-reminder>As you answer the user's questions, you can use the following context:
284
490
 
@@ -295,56 +501,29 @@ NEVER proactively create documentation files (*.md) or README files. Only create
295
501
  IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
296
502
  </system-reminder>""",
297
503
  memory_paths=[memory.path for memory in memories],
504
+ memory_mentioned=memory_mentioned or None,
298
505
  )
299
506
  return None
300
507
 
301
508
 
302
- def get_last_turn_tool_call(session: Session) -> list[model.ToolCallItem]:
303
- tool_calls: list[model.ToolCallItem] = []
304
- for item in reversed(session.conversation_history):
305
- if isinstance(item, model.ToolCallItem):
306
- tool_calls.append(item)
307
- if isinstance(
308
- item,
309
- (
310
- model.ReasoningEncryptedItem,
311
- model.ReasoningTextItem,
312
- model.AssistantMessageItem,
313
- ),
314
- ):
315
- break
316
- return tool_calls
317
-
318
-
319
509
  MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"]
320
510
 
321
511
 
322
512
  async def last_path_memory_reminder(
323
513
  session: Session,
324
514
  ) -> model.DeveloperMessageItem | None:
325
- """When last turn tool call entered a directory (or parent directory) with CLAUDE.md AGENTS.md"""
326
- tool_calls = get_last_turn_tool_call(session)
327
- if len(tool_calls) == 0:
515
+ """Load CLAUDE.md/AGENTS.md from directories containing files in file_tracker.
516
+
517
+ Uses session.file_tracker to detect accessed paths (works for both tool calls
518
+ and @ file references). Checks is_memory flag to avoid duplicate loading.
519
+ """
520
+ if not session.file_tracker:
328
521
  return None
329
- paths: list[str] = []
330
- for tool_call in tool_calls:
331
- if tool_call.name in (tools.READ, tools.EDIT, tools.MULTI_EDIT, tools.WRITE):
332
- try:
333
- json_dict = json.loads(tool_call.arguments)
334
- if path := json_dict.get("file_path", ""):
335
- paths.append(path)
336
- except json.JSONDecodeError:
337
- continue
338
- elif tool_call.name == tools.BASH:
339
- # TODO: haiku check file path
340
- pass
341
- paths = list(set(paths))
522
+
523
+ paths = list(session.file_tracker.keys())
342
524
  memories: list[Memory] = []
343
- if len(paths) == 0:
344
- return None
345
525
 
346
526
  cwd = Path.cwd().resolve()
347
- loaded_set: set[str] = set(session.loaded_memory)
348
527
  seen_memory_files: set[str] = set()
349
528
 
350
529
  for p_str in paths:
@@ -372,15 +551,14 @@ async def last_path_memory_reminder(
372
551
  for fname in MEMORY_FILE_NAMES:
373
552
  mem_path = current_dir / fname
374
553
  mem_path_str = str(mem_path)
375
- if mem_path_str in seen_memory_files or mem_path_str in loaded_set:
554
+ if mem_path_str in seen_memory_files or _is_memory_loaded(session, mem_path_str):
376
555
  continue
377
556
  if mem_path.exists() and mem_path.is_file():
378
557
  try:
379
558
  text = mem_path.read_text()
380
559
  except (PermissionError, UnicodeDecodeError, OSError):
381
560
  continue
382
- session.loaded_memory.append(mem_path_str)
383
- loaded_set.add(mem_path_str)
561
+ _mark_memory_loaded(session, mem_path_str)
384
562
  seen_memory_files.add(mem_path_str)
385
563
  memories.append(
386
564
  Memory(
@@ -394,10 +572,18 @@ async def last_path_memory_reminder(
394
572
  memories_str = "\n\n".join(
395
573
  [f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
396
574
  )
575
+ # Build memory_mentioned: extract @ patterns from each memory's content
576
+ memory_mentioned: dict[str, list[str]] = {}
577
+ for memory in memories:
578
+ patterns = _extract_at_patterns(memory.content)
579
+ if patterns:
580
+ memory_mentioned[memory.path] = patterns
581
+
397
582
  return model.DeveloperMessageItem(
398
583
  content=f"""<system-reminder>{memories_str}
399
584
  </system-reminder>""",
400
585
  memory_paths=[memory.path for memory in memories],
586
+ memory_mentioned=memory_mentioned or None,
401
587
  )
402
588
 
403
589
 
@@ -409,6 +595,7 @@ ALL_REMINDERS = [
409
595
  last_path_memory_reminder,
410
596
  at_file_reader_reminder,
411
597
  image_reminder,
598
+ skill_reminder,
412
599
  ]
413
600
 
414
601
 
@@ -435,10 +622,11 @@ def load_agent_reminders(
435
622
  reminders.extend(
436
623
  [
437
624
  memory_reminder,
438
- last_path_memory_reminder,
439
625
  at_file_reader_reminder,
626
+ last_path_memory_reminder,
440
627
  file_changed_externally_reminder,
441
628
  image_reminder,
629
+ skill_reminder,
442
630
  ]
443
631
  )
444
632