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.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
klaude_code/core/reminders.py
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
if
|
|
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 =
|
|
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,
|
|
304
|
+
for path, status in session.file_tracker.items():
|
|
184
305
|
try:
|
|
185
|
-
|
|
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
|
-
|
|
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
|
|
275
|
-
memories.append(Memory(path=
|
|
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
|
-
"""
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
|
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
|
|
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
|
|