klaude-code 1.2.20__py3-none-any.whl → 1.2.22__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 (42) hide show
  1. klaude_code/cli/debug.py +8 -10
  2. klaude_code/cli/main.py +23 -0
  3. klaude_code/cli/runtime.py +13 -1
  4. klaude_code/command/__init__.py +0 -3
  5. klaude_code/command/prompt-deslop.md +1 -1
  6. klaude_code/command/thinking_cmd.py +10 -0
  7. klaude_code/const/__init__.py +2 -5
  8. klaude_code/core/prompt.py +5 -2
  9. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  10. klaude_code/core/prompts/{prompt-codex-gpt-5-1.md → prompt-codex.md} +9 -42
  11. klaude_code/core/reminders.py +36 -2
  12. klaude_code/core/tool/__init__.py +0 -5
  13. klaude_code/core/tool/file/_utils.py +6 -0
  14. klaude_code/core/tool/file/apply_patch_tool.py +30 -72
  15. klaude_code/core/tool/file/diff_builder.py +151 -0
  16. klaude_code/core/tool/file/edit_tool.py +35 -18
  17. klaude_code/core/tool/file/read_tool.py +45 -86
  18. klaude_code/core/tool/file/write_tool.py +40 -30
  19. klaude_code/core/tool/shell/bash_tool.py +151 -3
  20. klaude_code/core/tool/web/mermaid_tool.md +26 -0
  21. klaude_code/protocol/commands.py +0 -1
  22. klaude_code/protocol/model.py +29 -10
  23. klaude_code/protocol/tools.py +1 -2
  24. klaude_code/session/export.py +75 -20
  25. klaude_code/session/session.py +7 -0
  26. klaude_code/session/templates/export_session.html +28 -0
  27. klaude_code/ui/renderers/common.py +26 -11
  28. klaude_code/ui/renderers/developer.py +0 -5
  29. klaude_code/ui/renderers/diffs.py +84 -0
  30. klaude_code/ui/renderers/tools.py +19 -98
  31. klaude_code/ui/rich/markdown.py +11 -1
  32. klaude_code/ui/rich/status.py +8 -11
  33. klaude_code/ui/rich/theme.py +14 -4
  34. {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/METADATA +2 -1
  35. {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/RECORD +37 -40
  36. klaude_code/command/diff_cmd.py +0 -136
  37. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  38. klaude_code/core/tool/file/multi_edit_tool.py +0 -175
  39. klaude_code/core/tool/memory/memory_tool.md +0 -20
  40. klaude_code/core/tool/memory/memory_tool.py +0 -456
  41. {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/WHEEL +0 -0
  42. {klaude_code-1.2.20.dist-info → klaude_code-1.2.22.dist-info}/entry_points.txt +0 -0
@@ -1,20 +0,0 @@
1
- Stores and retrieves information across conversations through a memory file directory. Use this tool to persist knowledge, progress, and context that should survive between sessions.
2
-
3
- The memory directory is located at `.claude/memories/` in the current project root (git repository root if present, otherwise the current working directory). Memories are scoped to the current project/directory and are not shared globally. All paths must start with `/memories` (e.g., `/memories/notes.txt`).
4
-
5
- Commands:
6
- - `view`: Show directory contents or file contents with optional line range
7
- - `create`: Create or overwrite a file
8
- - `str_replace`: Replace text in a file
9
- - `insert`: Insert text at a specific line
10
- - `delete`: Delete a file or directory
11
- - `rename`: Rename or move a file/directory
12
-
13
- Usage tips:
14
- - Check your memory directory before starting tasks to recall previous context
15
- - Record important decisions, progress, and learnings as you work
16
- - Keep memory files organized and up-to-date; delete obsolete files
17
-
18
- Note: when editing your memory folder, always try to keep its content up-to-date, coherent and organized. You can rename or delete files that are no longer relevant. Do not create new files unless necessary.
19
-
20
- Only write down information relevant to current project in your memory system.
@@ -1,456 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import difflib
5
- import os
6
- import shutil
7
- import subprocess
8
- import urllib.parse
9
- from pathlib import Path
10
- from typing import Literal
11
-
12
- from pydantic import BaseModel, Field
13
-
14
- from klaude_code.core.tool.tool_abc import ToolABC, load_desc
15
- from klaude_code.core.tool.tool_registry import register
16
- from klaude_code.protocol import llm_param, model, tools
17
-
18
- MEMORY_VIRTUAL_ROOT = "/memories"
19
- MEMORY_DIR_NAME = ".claude/memories"
20
-
21
-
22
- def _get_git_root() -> Path | None:
23
- """Get the git repository root directory."""
24
- try:
25
- result = subprocess.run(
26
- ["git", "rev-parse", "--show-toplevel"],
27
- capture_output=True,
28
- text=True,
29
- timeout=5,
30
- )
31
- if result.returncode == 0:
32
- return Path(result.stdout.strip())
33
- except Exception:
34
- pass
35
- return None
36
-
37
-
38
- def _get_memories_root() -> Path:
39
- """Get the actual memories directory path."""
40
- git_root = _get_git_root()
41
- if git_root is not None:
42
- return git_root / MEMORY_DIR_NAME
43
- return Path.cwd() / MEMORY_DIR_NAME
44
-
45
-
46
- def _ensure_memories_dir() -> Path:
47
- """Ensure the memories directory exists and return its path."""
48
- memories_root = _get_memories_root()
49
- memories_root.mkdir(parents=True, exist_ok=True)
50
- return memories_root
51
-
52
-
53
- def _validate_path(virtual_path: str) -> tuple[Path | None, str | None]:
54
- """
55
- Validate a virtual path and return the actual filesystem path.
56
-
57
- Returns:
58
- (actual_path, None) on success
59
- (None, error_message) on failure
60
- """
61
- # Check for URL-encoded traversal attempts
62
- decoded = urllib.parse.unquote(virtual_path)
63
- if ".." in decoded or ".." in virtual_path:
64
- return None, "Path traversal is not allowed"
65
-
66
- # Must start with /memories
67
- if not virtual_path.startswith(MEMORY_VIRTUAL_ROOT):
68
- return None, f"Path must start with {MEMORY_VIRTUAL_ROOT}"
69
-
70
- # Get relative path from /memories
71
- relative = "" if virtual_path == MEMORY_VIRTUAL_ROOT else virtual_path[len(MEMORY_VIRTUAL_ROOT) :].lstrip("/")
72
-
73
- memories_root = _get_memories_root()
74
- actual_path = memories_root / relative if relative else memories_root
75
-
76
- # Resolve to canonical path and verify it's still within memories
77
- try:
78
- resolved = actual_path.resolve()
79
- memories_resolved = memories_root.resolve()
80
- # Check if resolved path is within or equal to memories root
81
- try:
82
- resolved.relative_to(memories_resolved)
83
- except ValueError:
84
- # Also allow the exact memories root
85
- if resolved != memories_resolved:
86
- return None, "Path traversal is not allowed"
87
- except Exception as e:
88
- return None, f"Invalid path: {e}"
89
-
90
- return actual_path, None
91
-
92
-
93
- def _format_numbered_line(line_no: int, content: str) -> str:
94
- return f"{line_no:>6}|{content}"
95
-
96
-
97
- def _make_diff_ui_extra(before: str, after: str, path: str) -> model.DiffTextUIExtra:
98
- diff_lines = list(
99
- difflib.unified_diff(
100
- before.splitlines(),
101
- after.splitlines(),
102
- fromfile=path,
103
- tofile=path,
104
- n=3,
105
- )
106
- )
107
- diff_text = "\n".join(diff_lines)
108
- return model.DiffTextUIExtra(diff_text=diff_text)
109
-
110
-
111
- @register(tools.MEMORY)
112
- class MemoryTool(ToolABC):
113
- class MemoryArguments(BaseModel):
114
- command: Literal["view", "create", "str_replace", "insert", "delete", "rename"]
115
- path: str | None = Field(default=None)
116
- # view command
117
- view_range: list[int] | None = Field(default=None)
118
- # create command
119
- file_text: str | None = Field(default=None)
120
- # str_replace command
121
- old_str: str | None = Field(default=None)
122
- new_str: str | None = Field(default=None)
123
- # insert command
124
- insert_line: int | None = Field(default=None)
125
- insert_text: str | None = Field(default=None)
126
- # rename command
127
- old_path: str | None = Field(default=None)
128
- new_path: str | None = Field(default=None)
129
-
130
- @classmethod
131
- def schema(cls) -> llm_param.ToolSchema:
132
- return llm_param.ToolSchema(
133
- name=tools.MEMORY,
134
- type="function",
135
- description=load_desc(Path(__file__).parent / "memory_tool.md"),
136
- parameters={
137
- "type": "object",
138
- "properties": {
139
- "command": {
140
- "type": "string",
141
- "enum": [
142
- "view",
143
- "create",
144
- "str_replace",
145
- "insert",
146
- "delete",
147
- "rename",
148
- ],
149
- "description": "The memory operation to perform",
150
- },
151
- "path": {
152
- "type": "string",
153
- "description": "Path starting with /memories (for view, create, str_replace, insert, delete)",
154
- },
155
- "view_range": {
156
- "type": "array",
157
- "items": {"type": "integer"},
158
- "description": "Optional [start, end] line range for view command (1-indexed)",
159
- },
160
- "file_text": {
161
- "type": "string",
162
- "description": "Content to write (for create command)",
163
- },
164
- "old_str": {
165
- "type": "string",
166
- "description": "Text to find (for str_replace command)",
167
- },
168
- "new_str": {
169
- "type": "string",
170
- "description": "Text to replace with (for str_replace command)",
171
- },
172
- "insert_line": {
173
- "type": "integer",
174
- "description": "Line number to insert at (for insert command, 1-indexed)",
175
- },
176
- "insert_text": {
177
- "type": "string",
178
- "description": "Text to insert (for insert command)",
179
- },
180
- "old_path": {
181
- "type": "string",
182
- "description": "Source path (for rename command)",
183
- },
184
- "new_path": {
185
- "type": "string",
186
- "description": "Destination path (for rename command)",
187
- },
188
- },
189
- "required": ["command"],
190
- "additionalProperties": False,
191
- },
192
- )
193
-
194
- @classmethod
195
- async def call(cls, arguments: str) -> model.ToolResultItem:
196
- try:
197
- args = cls.MemoryArguments.model_validate_json(arguments)
198
- except Exception as e:
199
- return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
200
-
201
- command = args.command
202
- if command == "view":
203
- return await cls._view(args)
204
- elif command == "create":
205
- return await cls._create(args)
206
- elif command == "str_replace":
207
- return await cls._str_replace(args)
208
- elif command == "insert":
209
- return await cls._insert(args)
210
- elif command == "delete":
211
- return await cls._delete(args)
212
- elif command == "rename":
213
- return await cls._rename(args)
214
- else:
215
- return model.ToolResultItem(status="error", output=f"Unknown command: {command}")
216
-
217
- @classmethod
218
- async def _view(cls, args: MemoryArguments) -> model.ToolResultItem:
219
- if args.path is None:
220
- return model.ToolResultItem(status="error", output="path is required for view command")
221
-
222
- actual_path, error = _validate_path(args.path)
223
- if error:
224
- return model.ToolResultItem(status="error", output=error)
225
- assert actual_path is not None
226
-
227
- # Ensure memories directory exists
228
- _ensure_memories_dir()
229
-
230
- if not actual_path.exists():
231
- return model.ToolResultItem(status="error", output=f"Path does not exist: {args.path}")
232
-
233
- if actual_path.is_dir():
234
- # List directory contents
235
- try:
236
- entries = sorted(
237
- actual_path.iterdir(),
238
- key=lambda p: (not p.is_dir(), p.name.lower()),
239
- )
240
- lines = [f"Directory: {args.path}"]
241
- for entry in entries:
242
- prefix = "/" if entry.is_dir() else ""
243
- lines.append(f"- {entry.name}{prefix}")
244
- if len(entries) == 0:
245
- lines.append("(empty directory)")
246
- return model.ToolResultItem(status="success", output="\n".join(lines))
247
- except Exception as e:
248
- return model.ToolResultItem(status="error", output=f"Failed to list directory: {e}")
249
- else:
250
- # Read file contents
251
- try:
252
- content = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
253
- lines = content.splitlines()
254
- total_lines = len(lines)
255
-
256
- # Apply view_range if specified
257
- start = 1
258
- end = total_lines
259
- if args.view_range and len(args.view_range) >= 2:
260
- start = max(1, args.view_range[0])
261
- end = min(total_lines, args.view_range[1])
262
-
263
- if start > total_lines:
264
- return model.ToolResultItem(
265
- status="success",
266
- output=f"File has {total_lines} lines, requested start line {start} is beyond end of file",
267
- )
268
-
269
- selected = lines[start - 1 : end]
270
- numbered = [_format_numbered_line(start + i, line) for i, line in enumerate(selected)]
271
- output = "\n".join(numbered)
272
- if not output:
273
- output = "(empty file)"
274
- return model.ToolResultItem(status="success", output=output)
275
- except Exception as e:
276
- return model.ToolResultItem(status="error", output=f"Failed to read file: {e}")
277
-
278
- @classmethod
279
- async def _create(cls, args: MemoryArguments) -> model.ToolResultItem:
280
- if args.path is None:
281
- return model.ToolResultItem(status="error", output="path is required for create command")
282
- if args.file_text is None:
283
- return model.ToolResultItem(status="error", output="file_text is required for create command")
284
-
285
- actual_path, error = _validate_path(args.path)
286
- if error:
287
- return model.ToolResultItem(status="error", output=error)
288
- assert actual_path is not None
289
-
290
- # Cannot create the root directory itself
291
- if args.path == MEMORY_VIRTUAL_ROOT or args.path == MEMORY_VIRTUAL_ROOT + "/":
292
- return model.ToolResultItem(
293
- status="error",
294
- output="Cannot create the memories root directory as a file",
295
- )
296
-
297
- try:
298
- # Read existing content for diff (if file exists)
299
- before = ""
300
- if actual_path.exists():
301
- before = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
302
-
303
- # Ensure parent directories exist
304
- actual_path.parent.mkdir(parents=True, exist_ok=True)
305
- await asyncio.to_thread(actual_path.write_text, args.file_text, encoding="utf-8")
306
-
307
- ui_extra = _make_diff_ui_extra(before, args.file_text, args.path)
308
- return model.ToolResultItem(status="success", output=f"File created: {args.path}", ui_extra=ui_extra)
309
- except Exception as e:
310
- return model.ToolResultItem(status="error", output=f"Failed to create file: {e}")
311
-
312
- @classmethod
313
- async def _str_replace(cls, args: MemoryArguments) -> model.ToolResultItem:
314
- if args.path is None:
315
- return model.ToolResultItem(status="error", output="path is required for str_replace command")
316
- if args.old_str is None:
317
- return model.ToolResultItem(status="error", output="old_str is required for str_replace command")
318
- if args.new_str is None:
319
- return model.ToolResultItem(status="error", output="new_str is required for str_replace command")
320
-
321
- actual_path, error = _validate_path(args.path)
322
- if error:
323
- return model.ToolResultItem(status="error", output=error)
324
- assert actual_path is not None
325
-
326
- if not actual_path.exists():
327
- return model.ToolResultItem(status="error", output=f"File does not exist: {args.path}")
328
- if actual_path.is_dir():
329
- return model.ToolResultItem(status="error", output="Cannot perform str_replace on a directory")
330
-
331
- try:
332
- before = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
333
- if args.old_str not in before:
334
- return model.ToolResultItem(status="error", output=f"String not found in file: {args.old_str}")
335
-
336
- after = before.replace(args.old_str, args.new_str, 1)
337
- await asyncio.to_thread(actual_path.write_text, after, encoding="utf-8")
338
-
339
- ui_extra = _make_diff_ui_extra(before, after, args.path)
340
- return model.ToolResultItem(
341
- status="success",
342
- output=f"Replaced text in {args.path}",
343
- ui_extra=ui_extra,
344
- )
345
- except Exception as e:
346
- return model.ToolResultItem(status="error", output=f"Failed to replace text: {e}")
347
-
348
- @classmethod
349
- async def _insert(cls, args: MemoryArguments) -> model.ToolResultItem:
350
- if args.path is None:
351
- return model.ToolResultItem(status="error", output="path is required for insert command")
352
- if args.insert_line is None:
353
- return model.ToolResultItem(status="error", output="insert_line is required for insert command")
354
- if args.insert_text is None:
355
- return model.ToolResultItem(status="error", output="insert_text is required for insert command")
356
-
357
- actual_path, error = _validate_path(args.path)
358
- if error:
359
- return model.ToolResultItem(status="error", output=error)
360
- assert actual_path is not None
361
-
362
- if not actual_path.exists():
363
- return model.ToolResultItem(status="error", output=f"File does not exist: {args.path}")
364
- if actual_path.is_dir():
365
- return model.ToolResultItem(status="error", output="Cannot insert into a directory")
366
-
367
- try:
368
- before = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
369
- lines = before.splitlines(keepends=True)
370
-
371
- # Handle empty file
372
- if not lines:
373
- lines = []
374
-
375
- # Normalize insert_line (1-indexed)
376
- insert_idx = max(0, args.insert_line - 1)
377
- insert_idx = min(insert_idx, len(lines))
378
-
379
- # Ensure insert_text ends with newline if inserting in middle
380
- insert_text = args.insert_text
381
- if insert_idx < len(lines) and not insert_text.endswith("\n"):
382
- insert_text += "\n"
383
-
384
- lines.insert(insert_idx, insert_text)
385
- after = "".join(lines)
386
- await asyncio.to_thread(actual_path.write_text, after, encoding="utf-8")
387
-
388
- ui_extra = _make_diff_ui_extra(before, after, args.path)
389
- return model.ToolResultItem(
390
- status="success",
391
- output=f"Inserted text at line {args.insert_line} in {args.path}",
392
- ui_extra=ui_extra,
393
- )
394
- except Exception as e:
395
- return model.ToolResultItem(status="error", output=f"Failed to insert text: {e}")
396
-
397
- @classmethod
398
- async def _delete(cls, args: MemoryArguments) -> model.ToolResultItem:
399
- if args.path is None:
400
- return model.ToolResultItem(status="error", output="path is required for delete command")
401
-
402
- # Prevent deleting the root memories directory
403
- if args.path == MEMORY_VIRTUAL_ROOT or args.path == MEMORY_VIRTUAL_ROOT + "/":
404
- return model.ToolResultItem(status="error", output="Cannot delete the memories root directory")
405
-
406
- actual_path, error = _validate_path(args.path)
407
- if error:
408
- return model.ToolResultItem(status="error", output=error)
409
- assert actual_path is not None
410
-
411
- if not actual_path.exists():
412
- return model.ToolResultItem(status="error", output=f"Path does not exist: {args.path}")
413
-
414
- try:
415
- if actual_path.is_dir():
416
- await asyncio.to_thread(shutil.rmtree, actual_path)
417
- return model.ToolResultItem(status="success", output=f"Directory deleted: {args.path}")
418
- else:
419
- await asyncio.to_thread(os.remove, actual_path)
420
- return model.ToolResultItem(status="success", output=f"File deleted: {args.path}")
421
- except Exception as e:
422
- return model.ToolResultItem(status="error", output=f"Failed to delete: {e}")
423
-
424
- @classmethod
425
- async def _rename(cls, args: MemoryArguments) -> model.ToolResultItem:
426
- if args.old_path is None:
427
- return model.ToolResultItem(status="error", output="old_path is required for rename command")
428
- if args.new_path is None:
429
- return model.ToolResultItem(status="error", output="new_path is required for rename command")
430
-
431
- # Prevent renaming the root memories directory
432
- if args.old_path == MEMORY_VIRTUAL_ROOT or args.old_path == MEMORY_VIRTUAL_ROOT + "/":
433
- return model.ToolResultItem(status="error", output="Cannot rename the memories root directory")
434
-
435
- old_actual, error = _validate_path(args.old_path)
436
- if error:
437
- return model.ToolResultItem(status="error", output=f"Invalid old_path: {error}")
438
- assert old_actual is not None
439
-
440
- new_actual, error = _validate_path(args.new_path)
441
- if error:
442
- return model.ToolResultItem(status="error", output=f"Invalid new_path: {error}")
443
- assert new_actual is not None
444
-
445
- if not old_actual.exists():
446
- return model.ToolResultItem(status="error", output=f"Source path does not exist: {args.old_path}")
447
- if new_actual.exists():
448
- return model.ToolResultItem(status="error", output=f"Destination already exists: {args.new_path}")
449
-
450
- try:
451
- # Ensure parent directory of destination exists
452
- new_actual.parent.mkdir(parents=True, exist_ok=True)
453
- await asyncio.to_thread(shutil.move, str(old_actual), str(new_actual))
454
- return model.ToolResultItem(status="success", output=f"Renamed {args.old_path} to {args.new_path}")
455
- except Exception as e:
456
- return model.ToolResultItem(status="error", output=f"Failed to rename: {e}")