klaude-code 1.2.12__py3-none-any.whl → 1.2.14__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/codex/oauth.py +3 -3
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +88 -0
- klaude_code/cli/debug.py +72 -0
- klaude_code/cli/main.py +31 -142
- klaude_code/cli/runtime.py +19 -58
- klaude_code/cli/session_cmd.py +9 -9
- klaude_code/command/__init__.py +6 -6
- klaude_code/command/export_cmd.py +3 -3
- klaude_code/command/model_cmd.py +1 -1
- klaude_code/command/registry.py +1 -1
- klaude_code/command/terminal_setup_cmd.py +2 -2
- klaude_code/command/thinking_cmd.py +8 -6
- klaude_code/config/__init__.py +1 -5
- klaude_code/config/config.py +31 -4
- klaude_code/config/list_model.py +1 -1
- klaude_code/const/__init__.py +8 -3
- klaude_code/core/agent.py +14 -62
- klaude_code/core/executor.py +11 -10
- klaude_code/core/manager/agent_manager.py +4 -4
- klaude_code/core/manager/llm_clients.py +10 -49
- klaude_code/core/manager/llm_clients_builder.py +8 -21
- klaude_code/core/manager/sub_agent_manager.py +3 -3
- klaude_code/core/prompt.py +12 -7
- klaude_code/core/reminders.py +1 -1
- klaude_code/core/task.py +2 -2
- klaude_code/core/tool/__init__.py +16 -25
- klaude_code/core/tool/file/_utils.py +1 -1
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +4 -7
- klaude_code/core/tool/file/edit_tool.py +4 -11
- klaude_code/core/tool/file/multi_edit_tool.py +2 -3
- klaude_code/core/tool/file/read_tool.py +3 -4
- klaude_code/core/tool/file/write_tool.py +2 -3
- klaude_code/core/tool/memory/memory_tool.py +2 -8
- klaude_code/core/tool/memory/skill_loader.py +3 -2
- klaude_code/core/tool/shell/command_safety.py +0 -1
- klaude_code/core/tool/tool_context.py +1 -3
- klaude_code/core/tool/tool_registry.py +2 -1
- klaude_code/core/tool/tool_runner.py +1 -1
- klaude_code/core/tool/truncation.py +2 -5
- klaude_code/core/turn.py +9 -3
- klaude_code/llm/anthropic/client.py +6 -2
- klaude_code/llm/client.py +5 -1
- klaude_code/llm/codex/client.py +2 -2
- klaude_code/llm/input_common.py +2 -2
- klaude_code/llm/openai_compatible/client.py +11 -8
- klaude_code/llm/openai_compatible/stream_processor.py +2 -1
- klaude_code/llm/openrouter/client.py +22 -9
- klaude_code/llm/openrouter/reasoning_handler.py +19 -132
- klaude_code/llm/registry.py +6 -5
- klaude_code/llm/responses/client.py +10 -5
- klaude_code/protocol/events.py +9 -2
- klaude_code/protocol/model.py +7 -1
- klaude_code/protocol/sub_agent.py +2 -2
- klaude_code/session/export.py +58 -0
- klaude_code/session/selector.py +2 -2
- klaude_code/session/session.py +37 -7
- klaude_code/session/templates/export_session.html +46 -0
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +144 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/stage_manager.py +7 -4
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +1 -1
- klaude_code/ui/modes/repl/completers.py +6 -7
- klaude_code/ui/modes/repl/display.py +3 -4
- klaude_code/ui/modes/repl/event_handler.py +63 -5
- klaude_code/ui/modes/repl/key_bindings.py +2 -3
- klaude_code/ui/modes/repl/renderer.py +52 -62
- klaude_code/ui/renderers/diffs.py +1 -4
- klaude_code/ui/renderers/tools.py +4 -0
- klaude_code/ui/rich/markdown.py +3 -3
- klaude_code/ui/rich/searchable_text.py +6 -6
- klaude_code/ui/rich/status.py +3 -4
- klaude_code/ui/rich/theme.py +2 -5
- klaude_code/ui/terminal/control.py +7 -16
- klaude_code/ui/terminal/notifier.py +2 -4
- klaude_code/ui/utils/common.py +1 -1
- klaude_code/ui/utils/debouncer.py +2 -2
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.14.dist-info}/METADATA +1 -1
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.14.dist-info}/RECORD +84 -81
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.14.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.12.dist-info → klaude_code-1.2.14.dist-info}/entry_points.txt +0 -0
klaude_code/core/prompt.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import shutil
|
|
3
|
-
from functools import
|
|
3
|
+
from functools import cache
|
|
4
4
|
from importlib.resources import files
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
+
from klaude_code.protocol import llm_param
|
|
8
|
+
|
|
7
9
|
COMMAND_DESCRIPTIONS: dict[str, str] = {
|
|
8
10
|
"rg": "ripgrep - fast text search",
|
|
9
11
|
"fd": "simple and fast alternative to find",
|
|
@@ -25,7 +27,7 @@ PROMPT_FILES: dict[str, str] = {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
@
|
|
30
|
+
@cache
|
|
29
31
|
def _load_base_prompt(file_key: str) -> str:
|
|
30
32
|
"""Load and cache the base prompt content from file."""
|
|
31
33
|
try:
|
|
@@ -36,13 +38,13 @@ def _load_base_prompt(file_key: str) -> str:
|
|
|
36
38
|
return files(__package__).joinpath(prompt_path).read_text(encoding="utf-8").strip()
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
def _get_file_key(model_name: str, sub_agent_type: str | None) -> str:
|
|
41
|
+
def _get_file_key(model_name: str, protocol: llm_param.LLMClientProtocol, sub_agent_type: str | None) -> str:
|
|
40
42
|
"""Determine which prompt file to use based on model and agent type."""
|
|
41
43
|
if sub_agent_type is not None:
|
|
42
44
|
return sub_agent_type
|
|
43
45
|
|
|
44
46
|
match model_name:
|
|
45
|
-
case "gpt-5.1-codex-max":
|
|
47
|
+
case name if "gpt-5.1-codex-max" in name:
|
|
46
48
|
return "main_gpt_5_1_codex_max"
|
|
47
49
|
case name if "gpt-5" in name:
|
|
48
50
|
return "main_gpt_5_1"
|
|
@@ -84,12 +86,15 @@ def _build_env_info(model_name: str) -> str:
|
|
|
84
86
|
return "\n".join(env_lines)
|
|
85
87
|
|
|
86
88
|
|
|
87
|
-
def load_system_prompt(
|
|
89
|
+
def load_system_prompt(
|
|
90
|
+
model_name: str, protocol: llm_param.LLMClientProtocol, sub_agent_type: str | None = None
|
|
91
|
+
) -> str:
|
|
88
92
|
"""Get system prompt content for the given model and sub-agent type."""
|
|
89
|
-
file_key = _get_file_key(model_name, sub_agent_type)
|
|
93
|
+
file_key = _get_file_key(model_name, protocol, sub_agent_type)
|
|
90
94
|
base_prompt = _load_base_prompt(file_key)
|
|
91
95
|
|
|
92
|
-
if
|
|
96
|
+
if protocol == llm_param.LLMClientProtocol.CODEX:
|
|
97
|
+
# Do not append environment info for Codex protocol
|
|
93
98
|
return base_prompt
|
|
94
99
|
|
|
95
100
|
return base_prompt + _build_env_info(model_name)
|
klaude_code/core/reminders.py
CHANGED
klaude_code/core/task.py
CHANGED
|
@@ -119,7 +119,7 @@ class TaskExecutionContext:
|
|
|
119
119
|
profile: AgentProfile
|
|
120
120
|
tool_registry: dict[str, type[ToolABC]]
|
|
121
121
|
# For reminder processing - needs access to session
|
|
122
|
-
process_reminder: Callable[[Reminder], AsyncGenerator[events.DeveloperMessageEvent
|
|
122
|
+
process_reminder: Callable[[Reminder], AsyncGenerator[events.DeveloperMessageEvent]]
|
|
123
123
|
sub_agent_state: model.SubAgentState | None
|
|
124
124
|
|
|
125
125
|
|
|
@@ -146,7 +146,7 @@ class TaskExecutor:
|
|
|
146
146
|
self._current_turn = None
|
|
147
147
|
return ui_events
|
|
148
148
|
|
|
149
|
-
async def run(self, user_input: model.UserInputPayload) -> AsyncGenerator[events.Event
|
|
149
|
+
async def run(self, user_input: model.UserInputPayload) -> AsyncGenerator[events.Event]:
|
|
150
150
|
"""Execute the task, yielding events as they occur."""
|
|
151
151
|
ctx = self._context
|
|
152
152
|
session_ctx = ctx.session_ctx
|
|
@@ -29,49 +29,40 @@ from .web.mermaid_tool import MermaidTool
|
|
|
29
29
|
from .web.web_fetch_tool import WebFetchTool
|
|
30
30
|
|
|
31
31
|
__all__ = [
|
|
32
|
-
|
|
32
|
+
"MEMORY_DIR_NAME",
|
|
33
33
|
"ApplyPatchTool",
|
|
34
34
|
"BashTool",
|
|
35
|
+
"DiffError",
|
|
35
36
|
"EditTool",
|
|
36
37
|
"MemoryTool",
|
|
37
38
|
"MermaidTool",
|
|
38
39
|
"MultiEditTool",
|
|
39
40
|
"ReadTool",
|
|
41
|
+
"SafetyCheckResult",
|
|
42
|
+
"SimpleTruncationStrategy",
|
|
43
|
+
"Skill",
|
|
44
|
+
"SkillLoader",
|
|
40
45
|
"SkillTool",
|
|
41
46
|
"SubAgentTool",
|
|
47
|
+
"TodoContext",
|
|
42
48
|
"TodoWriteTool",
|
|
49
|
+
"ToolABC",
|
|
50
|
+
"ToolContextToken",
|
|
51
|
+
"TruncationStrategy",
|
|
43
52
|
"UpdatePlanTool",
|
|
44
53
|
"WebFetchTool",
|
|
45
54
|
"WriteTool",
|
|
46
|
-
# Tool ABC
|
|
47
|
-
"ToolABC",
|
|
48
|
-
# Tool context
|
|
49
|
-
"TodoContext",
|
|
50
55
|
"build_todo_context",
|
|
51
|
-
"ToolContextToken",
|
|
52
56
|
"current_run_subtask_callback",
|
|
53
|
-
"reset_tool_context",
|
|
54
|
-
"set_tool_context_from_session",
|
|
55
|
-
"tool_context",
|
|
56
|
-
# Tool registry
|
|
57
|
-
"load_agent_tools",
|
|
58
57
|
"get_registry",
|
|
59
58
|
"get_tool_schemas",
|
|
60
|
-
"run_tool",
|
|
61
|
-
# Truncation
|
|
62
|
-
"SimpleTruncationStrategy",
|
|
63
|
-
"TruncationStrategy",
|
|
64
59
|
"get_truncation_strategy",
|
|
65
|
-
"set_truncation_strategy",
|
|
66
|
-
# Command safety
|
|
67
|
-
"SafetyCheckResult",
|
|
68
60
|
"is_safe_command",
|
|
69
|
-
|
|
70
|
-
"Skill",
|
|
71
|
-
"SkillLoader",
|
|
72
|
-
# Memory
|
|
73
|
-
"MEMORY_DIR_NAME",
|
|
74
|
-
# Apply patch
|
|
75
|
-
"DiffError",
|
|
61
|
+
"load_agent_tools",
|
|
76
62
|
"process_patch",
|
|
63
|
+
"reset_tool_context",
|
|
64
|
+
"run_tool",
|
|
65
|
+
"set_tool_context_from_session",
|
|
66
|
+
"set_truncation_strategy",
|
|
67
|
+
"tool_context",
|
|
77
68
|
]
|
|
@@ -18,7 +18,7 @@ def file_exists(path: str) -> bool:
|
|
|
18
18
|
|
|
19
19
|
def read_text(path: str) -> str:
|
|
20
20
|
"""Read text from file with UTF-8 encoding."""
|
|
21
|
-
with open(path,
|
|
21
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
22
22
|
return f.read()
|
|
23
23
|
|
|
24
24
|
|
|
@@ -3,8 +3,8 @@ https://github.com/openai/openai-cookbook/blob/main/examples/gpt-5/apply_patch.p
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from enum import Enum
|
|
7
|
-
from typing import Callable, Optional
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
@@ -17,16 +17,16 @@ class ActionType(str, Enum):
|
|
|
17
17
|
|
|
18
18
|
class FileChange(BaseModel):
|
|
19
19
|
type: ActionType
|
|
20
|
-
old_content:
|
|
21
|
-
new_content:
|
|
22
|
-
move_path:
|
|
20
|
+
old_content: str | None = None
|
|
21
|
+
new_content: str | None = None
|
|
22
|
+
move_path: str | None = None
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class Commit(BaseModel):
|
|
26
26
|
changes: dict[str, FileChange] = Field(default_factory=dict)
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def assemble_changes(orig: dict[str,
|
|
29
|
+
def assemble_changes(orig: dict[str, str | None], dest: dict[str, str | None]) -> Commit:
|
|
30
30
|
commit = Commit()
|
|
31
31
|
for path in sorted(set(orig.keys()).union(dest.keys())):
|
|
32
32
|
old_content = orig.get(path)
|
|
@@ -49,7 +49,7 @@ def assemble_changes(orig: dict[str, Optional[str]], dest: dict[str, Optional[st
|
|
|
49
49
|
old_content=old_content,
|
|
50
50
|
)
|
|
51
51
|
else:
|
|
52
|
-
|
|
52
|
+
raise AssertionError()
|
|
53
53
|
return commit
|
|
54
54
|
|
|
55
55
|
|
|
@@ -71,9 +71,9 @@ def _new_chunk_list() -> list["Chunk"]:
|
|
|
71
71
|
|
|
72
72
|
class PatchAction(BaseModel):
|
|
73
73
|
type: ActionType
|
|
74
|
-
new_file:
|
|
74
|
+
new_file: str | None = None
|
|
75
75
|
chunks: list[Chunk] = Field(default_factory=_new_chunk_list)
|
|
76
|
-
move_path:
|
|
76
|
+
move_path: str | None = None
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
class Patch(BaseModel):
|
|
@@ -87,26 +87,19 @@ class Parser(BaseModel):
|
|
|
87
87
|
patch: Patch = Field(default_factory=Patch)
|
|
88
88
|
fuzz: int = 0
|
|
89
89
|
|
|
90
|
-
def is_done(self, prefixes:
|
|
90
|
+
def is_done(self, prefixes: tuple[str, ...] | None = None) -> bool:
|
|
91
91
|
if self.index >= len(self.lines):
|
|
92
92
|
return True
|
|
93
|
-
|
|
94
|
-
return True
|
|
95
|
-
return False
|
|
93
|
+
return bool(prefixes and self.lines[self.index].startswith(prefixes))
|
|
96
94
|
|
|
97
95
|
def startswith(self, prefix: str | tuple[str, ...]) -> bool:
|
|
98
96
|
assert self.index < len(self.lines), f"Index: {self.index} >= {len(self.lines)}"
|
|
99
|
-
|
|
100
|
-
return True
|
|
101
|
-
return False
|
|
97
|
+
return self.lines[self.index].startswith(prefix)
|
|
102
98
|
|
|
103
99
|
def read_str(self, prefix: str = "", return_everything: bool = False) -> str:
|
|
104
100
|
assert self.index < len(self.lines), f"Index: {self.index} >= {len(self.lines)}"
|
|
105
101
|
if self.lines[self.index].startswith(prefix):
|
|
106
|
-
if return_everything:
|
|
107
|
-
text = self.lines[self.index]
|
|
108
|
-
else:
|
|
109
|
-
text = self.lines[self.index][len(prefix) :]
|
|
102
|
+
text = self.lines[self.index] if return_everything else self.lines[self.index][len(prefix) :]
|
|
110
103
|
self.index += 1
|
|
111
104
|
return text
|
|
112
105
|
return ""
|
|
@@ -167,10 +160,9 @@ class Parser(BaseModel):
|
|
|
167
160
|
):
|
|
168
161
|
def_str = self.read_str("@@ ")
|
|
169
162
|
section_str = ""
|
|
170
|
-
if not def_str:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
self.index += 1
|
|
163
|
+
if not def_str and self.lines[self.index] == "@@":
|
|
164
|
+
section_str = self.lines[self.index]
|
|
165
|
+
self.index += 1
|
|
174
166
|
if not (def_str or section_str or index == 0):
|
|
175
167
|
raise DiffError(f"Invalid Line:\n{self.lines[self.index]}")
|
|
176
168
|
if def_str.strip():
|
|
@@ -457,7 +449,7 @@ def process_patch(
|
|
|
457
449
|
|
|
458
450
|
|
|
459
451
|
def open_file(path: str) -> str:
|
|
460
|
-
with open(path
|
|
452
|
+
with open(path) as f:
|
|
461
453
|
return f.read()
|
|
462
454
|
|
|
463
455
|
|
|
@@ -465,7 +457,7 @@ def write_file(path: str, content: str) -> None:
|
|
|
465
457
|
if "/" in path:
|
|
466
458
|
parent = "/".join(path.split("/")[:-1])
|
|
467
459
|
os.makedirs(parent, exist_ok=True)
|
|
468
|
-
with open(path, "
|
|
460
|
+
with open(path, "w") as f:
|
|
469
461
|
f.write(content)
|
|
470
462
|
|
|
471
463
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""ApplyPatch tool providing direct patch application capability."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import difflib
|
|
5
6
|
import os
|
|
6
7
|
from pathlib import Path
|
|
@@ -58,7 +59,7 @@ class ApplyPatchHandler:
|
|
|
58
59
|
if os.path.isdir(resolved):
|
|
59
60
|
raise ap.DiffError(f"Cannot apply patch to directory: {path}")
|
|
60
61
|
try:
|
|
61
|
-
with open(resolved,
|
|
62
|
+
with open(resolved, encoding="utf-8") as handle:
|
|
62
63
|
orig[path] = handle.read()
|
|
63
64
|
except OSError as error:
|
|
64
65
|
raise ap.DiffError(f"Failed to read {path}: {error}") from error
|
|
@@ -78,10 +79,8 @@ class ApplyPatchHandler:
|
|
|
78
79
|
handle.write(content)
|
|
79
80
|
|
|
80
81
|
if file_tracker is not None:
|
|
81
|
-
|
|
82
|
+
with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
|
|
82
83
|
file_tracker[resolved] = Path(resolved).stat().st_mtime
|
|
83
|
-
except Exception: # pragma: no cover - file tracker best-effort
|
|
84
|
-
pass
|
|
85
84
|
|
|
86
85
|
def remove_fn(path: str) -> None:
|
|
87
86
|
resolved = resolve_path(path)
|
|
@@ -92,10 +91,8 @@ class ApplyPatchHandler:
|
|
|
92
91
|
os.remove(resolved)
|
|
93
92
|
|
|
94
93
|
if file_tracker is not None:
|
|
95
|
-
|
|
94
|
+
with contextlib.suppress(Exception): # pragma: no cover - file tracker best-effort
|
|
96
95
|
file_tracker.pop(resolved, None)
|
|
97
|
-
except Exception: # pragma: no cover - file tracker best-effort
|
|
98
|
-
pass
|
|
99
96
|
|
|
100
97
|
ap.apply_commit(commit, write_fn, remove_fn)
|
|
101
98
|
return "Done!", diff_text
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import difflib
|
|
5
6
|
import os
|
|
6
7
|
from pathlib import Path
|
|
@@ -191,10 +192,8 @@ class EditTool(ToolABC):
|
|
|
191
192
|
|
|
192
193
|
# Update tracker with new mtime
|
|
193
194
|
if file_tracker is not None:
|
|
194
|
-
|
|
195
|
+
with contextlib.suppress(Exception):
|
|
195
196
|
file_tracker[file_path] = Path(file_path).stat().st_mtime
|
|
196
|
-
except Exception:
|
|
197
|
-
pass
|
|
198
197
|
|
|
199
198
|
# Build output message
|
|
200
199
|
if args.replace_all:
|
|
@@ -213,18 +212,12 @@ class EditTool(ToolABC):
|
|
|
213
212
|
header = line
|
|
214
213
|
plus = header.split("+", 1)[1]
|
|
215
214
|
plus_range = plus.split(" ")[0]
|
|
216
|
-
if "," in plus_range
|
|
217
|
-
start = int(plus_range.split(",")[0])
|
|
218
|
-
else:
|
|
219
|
-
start = int(plus_range)
|
|
215
|
+
start = int(plus_range.split(",")[0]) if "," in plus_range else int(plus_range)
|
|
220
216
|
after_line_no = start - 1
|
|
221
217
|
except Exception:
|
|
222
218
|
after_line_no = 0
|
|
223
219
|
continue
|
|
224
|
-
if line.startswith(" "):
|
|
225
|
-
after_line_no += 1
|
|
226
|
-
include_after_line_nos.append(after_line_no)
|
|
227
|
-
elif line.startswith("+") and not line.startswith("+++ "):
|
|
220
|
+
if line.startswith(" ") or (line.startswith("+") and not line.startswith("+++ ")):
|
|
228
221
|
after_line_no += 1
|
|
229
222
|
include_after_line_nos.append(after_line_no)
|
|
230
223
|
elif line.startswith("-") and not line.startswith("--- "):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import difflib
|
|
5
6
|
import os
|
|
6
7
|
from pathlib import Path
|
|
@@ -162,10 +163,8 @@ class MultiEditTool(ToolABC):
|
|
|
162
163
|
|
|
163
164
|
# Update tracker
|
|
164
165
|
if file_tracker is not None:
|
|
165
|
-
|
|
166
|
+
with contextlib.suppress(Exception):
|
|
166
167
|
file_tracker[file_path] = Path(file_path).stat().st_mtime
|
|
167
|
-
except Exception:
|
|
168
|
-
pass
|
|
169
168
|
|
|
170
169
|
# Build output message
|
|
171
170
|
lines = [f"Applied {len(args.edits)} edits to {file_path}:"]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import os
|
|
5
6
|
from base64 import b64encode
|
|
6
7
|
from dataclasses import dataclass
|
|
@@ -58,7 +59,7 @@ def _read_segment(options: ReadOptions) -> ReadSegmentResult:
|
|
|
58
59
|
remaining_selected_beyond_cap = 0
|
|
59
60
|
selected_lines: list[tuple[int, str]] = []
|
|
60
61
|
selected_chars = 0
|
|
61
|
-
with open(options.file_path,
|
|
62
|
+
with open(options.file_path, encoding="utf-8", errors="replace") as f:
|
|
62
63
|
for line_no, raw_line in enumerate(f, start=1):
|
|
63
64
|
total_lines = line_no
|
|
64
65
|
within = line_no >= options.offset and (options.limit is None or selected_lines_count < options.limit)
|
|
@@ -90,10 +91,8 @@ def _track_file_access(file_path: str) -> None:
|
|
|
90
91
|
file_tracker = get_current_file_tracker()
|
|
91
92
|
if file_tracker is None or not file_exists(file_path) or is_directory(file_path):
|
|
92
93
|
return
|
|
93
|
-
|
|
94
|
+
with contextlib.suppress(Exception):
|
|
94
95
|
file_tracker[file_path] = Path(file_path).stat().st_mtime
|
|
95
|
-
except Exception:
|
|
96
|
-
pass
|
|
97
96
|
|
|
98
97
|
|
|
99
98
|
def _is_supported_image_file(file_path: str) -> bool:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import difflib
|
|
5
6
|
import os
|
|
6
7
|
from pathlib import Path
|
|
@@ -98,10 +99,8 @@ class WriteTool(ToolABC):
|
|
|
98
99
|
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
99
100
|
|
|
100
101
|
if file_tracker is not None:
|
|
101
|
-
|
|
102
|
+
with contextlib.suppress(Exception):
|
|
102
103
|
file_tracker[file_path] = Path(file_path).stat().st_mtime
|
|
103
|
-
except Exception:
|
|
104
|
-
pass
|
|
105
104
|
|
|
106
105
|
# Build diff between previous and new content
|
|
107
106
|
after = args.content
|
|
@@ -68,16 +68,10 @@ def _validate_path(virtual_path: str) -> tuple[Path | None, str | None]:
|
|
|
68
68
|
return None, f"Path must start with {MEMORY_VIRTUAL_ROOT}"
|
|
69
69
|
|
|
70
70
|
# Get relative path from /memories
|
|
71
|
-
if virtual_path == MEMORY_VIRTUAL_ROOT:
|
|
72
|
-
relative = ""
|
|
73
|
-
else:
|
|
74
|
-
relative = virtual_path[len(MEMORY_VIRTUAL_ROOT) :].lstrip("/")
|
|
71
|
+
relative = "" if virtual_path == MEMORY_VIRTUAL_ROOT else virtual_path[len(MEMORY_VIRTUAL_ROOT) :].lstrip("/")
|
|
75
72
|
|
|
76
73
|
memories_root = _get_memories_root()
|
|
77
|
-
if relative
|
|
78
|
-
actual_path = memories_root / relative
|
|
79
|
-
else:
|
|
80
|
-
actual_path = memories_root
|
|
74
|
+
actual_path = memories_root / relative if relative else memories_root
|
|
81
75
|
|
|
82
76
|
# Resolve to canonical path and verify it's still within memories
|
|
83
77
|
try:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import ClassVar
|
|
4
5
|
|
|
5
6
|
import yaml
|
|
6
7
|
|
|
@@ -36,13 +37,13 @@ class SkillLoader:
|
|
|
36
37
|
"""Load and manage Claude Skills from SKILL.md files"""
|
|
37
38
|
|
|
38
39
|
# User-level skills directories (checked in order, later ones override earlier ones with same name)
|
|
39
|
-
USER_SKILLS_DIRS = [
|
|
40
|
+
USER_SKILLS_DIRS: ClassVar[list[Path]] = [
|
|
40
41
|
Path("~/.claude/skills"),
|
|
41
42
|
Path("~/.klaude/skills"),
|
|
42
43
|
# Path("~/.claude/plugins/marketplaces"),
|
|
43
44
|
]
|
|
44
45
|
# Project-level skills directory
|
|
45
|
-
PROJECT_SKILLS_DIR = Path("./.claude/skills")
|
|
46
|
+
PROJECT_SKILLS_DIR: ClassVar[Path] = Path("./.claude/skills")
|
|
46
47
|
|
|
47
48
|
def __init__(self) -> None:
|
|
48
49
|
"""Initialize the skill loader"""
|
|
@@ -83,9 +83,7 @@ def reset_tool_context(token: ToolContextToken) -> None:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
@contextmanager
|
|
86
|
-
def tool_context(
|
|
87
|
-
file_tracker: MutableMapping[str, float], todo_ctx: TodoContext
|
|
88
|
-
) -> Generator[ToolContextToken, None, None]:
|
|
86
|
+
def tool_context(file_tracker: MutableMapping[str, float], todo_ctx: TodoContext) -> Generator[ToolContextToken]:
|
|
89
87
|
"""Context manager for setting and resetting tool execution context."""
|
|
90
88
|
|
|
91
89
|
file_tracker_token = current_file_tracker_var.set(file_tracker)
|
|
@@ -100,7 +100,7 @@ class ToolExecutor:
|
|
|
100
100
|
self._call_event_emitted: set[str] = set()
|
|
101
101
|
self._sub_agent_tasks: set[asyncio.Task[list[ToolExecutorEvent]]] = set()
|
|
102
102
|
|
|
103
|
-
async def run_tools(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[ToolExecutorEvent
|
|
103
|
+
async def run_tools(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[ToolExecutorEvent]:
|
|
104
104
|
"""Run the given tool calls and yield execution events.
|
|
105
105
|
|
|
106
106
|
Tool calls are partitioned into regular tools and sub-agent tools. Regular tools
|
|
@@ -27,10 +27,7 @@ def _extract_url_filename(url: str) -> str:
|
|
|
27
27
|
# Combine host and path for a meaningful filename
|
|
28
28
|
host = parsed.netloc.replace(".", "_").replace(":", "_")
|
|
29
29
|
path = parsed.path.strip("/").replace("/", "_")
|
|
30
|
-
if path
|
|
31
|
-
name = f"{host}_{path}"
|
|
32
|
-
else:
|
|
33
|
-
name = host
|
|
30
|
+
name = f"{host}_{path}" if path else host
|
|
34
31
|
# Sanitize: keep only alphanumeric, underscore, hyphen
|
|
35
32
|
name = re.sub(r"[^a-zA-Z0-9_\-]", "_", name)
|
|
36
33
|
# Limit length
|
|
@@ -106,7 +103,7 @@ class SmartTruncationStrategy(TruncationStrategy):
|
|
|
106
103
|
file_path = self.truncation_dir / filename
|
|
107
104
|
file_path.write_text(output, encoding="utf-8")
|
|
108
105
|
return str(file_path)
|
|
109
|
-
except
|
|
106
|
+
except OSError:
|
|
110
107
|
return None
|
|
111
108
|
|
|
112
109
|
def truncate(self, output: str, tool_call: model.ToolCallItem | None = None) -> TruncationResult:
|
klaude_code/core/turn.py
CHANGED
|
@@ -114,7 +114,7 @@ class TurnExecutor:
|
|
|
114
114
|
self._tool_executor = None
|
|
115
115
|
return ui_events
|
|
116
116
|
|
|
117
|
-
async def run(self) -> AsyncGenerator[events.Event
|
|
117
|
+
async def run(self) -> AsyncGenerator[events.Event]:
|
|
118
118
|
"""Execute the turn, yielding events as they occur.
|
|
119
119
|
|
|
120
120
|
Raises:
|
|
@@ -148,7 +148,7 @@ class TurnExecutor:
|
|
|
148
148
|
|
|
149
149
|
yield events.TurnEndEvent(session_id=session_ctx.session_id)
|
|
150
150
|
|
|
151
|
-
async def _consume_llm_stream(self, turn_result: TurnResult) -> AsyncGenerator[events.Event
|
|
151
|
+
async def _consume_llm_stream(self, turn_result: TurnResult) -> AsyncGenerator[events.Event]:
|
|
152
152
|
"""Stream events from LLM and update turn_result in place."""
|
|
153
153
|
|
|
154
154
|
ctx = self._context
|
|
@@ -179,6 +179,12 @@ class TurnExecutor:
|
|
|
179
179
|
)
|
|
180
180
|
case model.ReasoningEncryptedItem() as item:
|
|
181
181
|
turn_result.reasoning_items.append(item)
|
|
182
|
+
case model.ReasoningTextDelta() as item:
|
|
183
|
+
yield events.ThinkingDeltaEvent(
|
|
184
|
+
content=item.content,
|
|
185
|
+
response_id=item.response_id,
|
|
186
|
+
session_id=session_ctx.session_id,
|
|
187
|
+
)
|
|
182
188
|
case model.AssistantMessageDelta() as item:
|
|
183
189
|
yield events.AssistantMessageDeltaEvent(
|
|
184
190
|
content=item.content,
|
|
@@ -228,7 +234,7 @@ class TurnExecutor:
|
|
|
228
234
|
if turn_result.tool_calls:
|
|
229
235
|
session_ctx.append_history(turn_result.tool_calls)
|
|
230
236
|
|
|
231
|
-
async def _run_tool_executor(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[events.Event
|
|
237
|
+
async def _run_tool_executor(self, tool_calls: list[model.ToolCallItem]) -> AsyncGenerator[events.Event]:
|
|
232
238
|
"""Run tools for the turn and translate executor events to UI events."""
|
|
233
239
|
|
|
234
240
|
ctx = self._context
|
|
@@ -74,7 +74,7 @@ class AnthropicClient(LLMClientABC):
|
|
|
74
74
|
return cls(config)
|
|
75
75
|
|
|
76
76
|
@override
|
|
77
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem
|
|
77
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
|
|
78
78
|
param = apply_config_defaults(param, self.get_llm_config())
|
|
79
79
|
|
|
80
80
|
metadata_tracker = MetadataTracker(cost_config=self.get_llm_config().cost)
|
|
@@ -122,6 +122,10 @@ class AnthropicClient(LLMClientABC):
|
|
|
122
122
|
case BetaThinkingDelta() as delta:
|
|
123
123
|
metadata_tracker.record_token()
|
|
124
124
|
accumulated_thinking.append(delta.thinking)
|
|
125
|
+
yield model.ReasoningTextDelta(
|
|
126
|
+
content=delta.thinking,
|
|
127
|
+
response_id=response_id,
|
|
128
|
+
)
|
|
125
129
|
case BetaSignatureDelta() as delta:
|
|
126
130
|
metadata_tracker.record_token()
|
|
127
131
|
yield model.ReasoningEncryptedItem(
|
|
@@ -197,4 +201,4 @@ class AnthropicClient(LLMClientABC):
|
|
|
197
201
|
case _:
|
|
198
202
|
pass
|
|
199
203
|
except (APIError, httpx.HTTPError) as e:
|
|
200
|
-
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {
|
|
204
|
+
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
|
klaude_code/llm/client.py
CHANGED
|
@@ -15,7 +15,7 @@ class LLMClientABC(ABC):
|
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
@abstractmethod
|
|
18
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem
|
|
18
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
|
|
19
19
|
raise NotImplementedError
|
|
20
20
|
yield cast(model.ConversationItem, None)
|
|
21
21
|
|
|
@@ -26,6 +26,10 @@ class LLMClientABC(ABC):
|
|
|
26
26
|
def model_name(self) -> str:
|
|
27
27
|
return self._config.model or ""
|
|
28
28
|
|
|
29
|
+
@property
|
|
30
|
+
def protocol(self) -> llm_param.LLMClientProtocol:
|
|
31
|
+
return self._config.protocol
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
P = ParamSpec("P")
|
|
31
35
|
R = TypeVar("R")
|
klaude_code/llm/codex/client.py
CHANGED
|
@@ -114,7 +114,7 @@ class CodexClient(LLMClientABC):
|
|
|
114
114
|
return cls(config)
|
|
115
115
|
|
|
116
116
|
@override
|
|
117
|
-
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem
|
|
117
|
+
async def call(self, param: llm_param.LLMCallParameter) -> AsyncGenerator[model.ConversationItem]:
|
|
118
118
|
# Ensure token is valid before API call
|
|
119
119
|
self._ensure_valid_token()
|
|
120
120
|
|
|
@@ -142,7 +142,7 @@ class CodexClient(LLMClientABC):
|
|
|
142
142
|
extra_headers=extra_headers,
|
|
143
143
|
)
|
|
144
144
|
except (openai.OpenAIError, httpx.HTTPError) as e:
|
|
145
|
-
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {
|
|
145
|
+
yield model.StreamErrorItem(error=f"{e.__class__.__name__} {e!s}")
|
|
146
146
|
return
|
|
147
147
|
|
|
148
148
|
async for item in parse_responses_stream(stream, param, metadata_tracker):
|
klaude_code/llm/input_common.py
CHANGED
|
@@ -5,10 +5,10 @@ This module provides shared abstractions for providers that require message grou
|
|
|
5
5
|
since it uses a flat item list matching our internal protocol.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from collections.abc import Iterator
|
|
8
|
+
from collections.abc import Iterable, Iterator
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from enum import Enum
|
|
11
|
-
from typing import TYPE_CHECKING
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
13
|
from klaude_code import const
|
|
14
14
|
|