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
klaude_code/cli/main.py CHANGED
@@ -1,117 +1,124 @@
1
1
  import asyncio
2
+ import contextlib
2
3
  import os
3
- import subprocess
4
4
  import sys
5
- import uuid
6
- from importlib.metadata import version as pkg_version
5
+ from pathlib import Path
7
6
 
8
7
  import typer
9
8
 
10
- from klaude_code.cli.runtime import DEBUG_FILTER_HELP, AppInitConfig, resolve_debug_settings, run_exec, run_interactive
9
+ from klaude_code.cli.auth_cmd import register_auth_commands
10
+ from klaude_code.cli.config_cmd import register_config_commands
11
+ from klaude_code.cli.cost_cmd import register_cost_commands
12
+ from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, resolve_debug_settings
13
+ from klaude_code.cli.self_update import register_self_update_commands, version_option_callback
11
14
  from klaude_code.cli.session_cmd import register_session_commands
12
- from klaude_code.config import config_path, display_models_and_providers, load_config, select_model_from_config
13
- from klaude_code.session import Session, resume_select_session
14
- from klaude_code.trace import log
15
- from klaude_code.ui.terminal.color import is_light_terminal_background
15
+ from klaude_code.command.resume_cmd import select_session_sync
16
+ from klaude_code.session import Session
17
+ from klaude_code.trace import DebugType, prepare_debug_log_file
16
18
 
17
19
 
18
20
  def set_terminal_title(title: str) -> None:
19
21
  """Set terminal window title using ANSI escape sequence."""
20
- sys.stdout.write(f"\033]0;{title}\007")
21
- sys.stdout.flush()
22
+ # Never write terminal control sequences when stdout is not a TTY (pipes/CI/redirects).
23
+ # This avoids corrupting machine-readable output (e.g., JSON streaming) and log captures.
24
+ #
25
+ # Use the original stdout to bypass prompt_toolkit's `patch_stdout()`. Writing OSC
26
+ # sequences to the patched stdout can cause them to appear as visible text.
27
+ stream = getattr(sys, "__stdout__", None) or sys.stdout
28
+ try:
29
+ if not stream.isatty():
30
+ return
31
+ except Exception:
32
+ return
33
+
34
+ stream.write(f"\033]0;{title}\007")
35
+ with contextlib.suppress(Exception):
36
+ stream.flush()
37
+
38
+
39
+ def update_terminal_title(model_name: str | None = None) -> None:
40
+ """Update terminal title with folder name and optional model name."""
41
+ folder_name = os.path.basename(os.getcwd())
42
+ if model_name:
43
+ set_terminal_title(f"{folder_name}: klaude ✳ {model_name}")
44
+ else:
45
+ set_terminal_title(f"{folder_name}: klaude")
46
+
47
+
48
+ def prepare_debug_logging(debug: bool, debug_filter: str | None) -> tuple[bool, set[DebugType] | None, Path | None]:
49
+ """Resolve debug settings and prepare log file if debugging is enabled.
50
+
51
+ Returns:
52
+ A tuple of (debug_enabled, debug_filters, log_path).
53
+ log_path is None if debugging is disabled.
54
+ """
55
+ debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
56
+ log_path: Path | None = None
57
+ if debug_enabled:
58
+ log_path = prepare_debug_log_file()
59
+ return debug_enabled, debug_filters, log_path
60
+
61
+
62
+ def read_input_content(cli_argument: str) -> str | None:
63
+ """Read and merge input from stdin and CLI argument.
64
+
65
+ Args:
66
+ cli_argument: The input content passed as CLI argument.
67
+
68
+ Returns:
69
+ The merged input content, or None if no input was provided.
70
+ """
71
+ from klaude_code.trace import log
22
72
 
73
+ parts: list[str] = []
23
74
 
24
- def _version_callback(value: bool) -> None:
25
- """Show version and exit."""
26
- if value:
75
+ # Handle stdin input
76
+ if not sys.stdin.isatty():
27
77
  try:
28
- ver = pkg_version("klaude-code")
29
- except Exception:
30
- ver = "unknown"
31
- print(f"klaude-code {ver}")
32
- raise typer.Exit(0)
78
+ stdin = sys.stdin.read().rstrip("\n")
79
+ if stdin:
80
+ parts.append(stdin)
81
+ except (OSError, ValueError) as e:
82
+ # Expected I/O-related errors when reading from stdin (e.g. broken pipe, closed stream).
83
+ log((f"Error reading from stdin: {e}", "red"))
84
+ except Exception as e:
85
+ # Unexpected errors are still reported but kept from crashing the CLI.
86
+ log((f"Unexpected error reading from stdin: {e}", "red"))
87
+
88
+ if cli_argument:
89
+ parts.append(cli_argument)
90
+
91
+ content = "\n".join(parts)
92
+ if len(content) == 0:
93
+ log(("Error: No input content provided", "red"))
94
+ return None
95
+
96
+ return content
97
+
98
+
99
+ ENV_HELP = """\
100
+ Environment Variables:
101
+
102
+ KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)
33
103
 
104
+ KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)
105
+ """
34
106
 
35
107
  app = typer.Typer(
36
108
  add_completion=False,
37
109
  pretty_exceptions_enable=False,
38
110
  no_args_is_help=False,
111
+ rich_markup_mode="rich",
112
+ epilog=ENV_HELP,
39
113
  )
40
114
 
41
- session_app = typer.Typer(help="Manage sessions for the current project")
42
- register_session_commands(session_app)
43
- app.add_typer(session_app, name="session")
115
+ # Register subcommands from modules
116
+ register_session_commands(app)
117
+ register_auth_commands(app)
118
+ register_config_commands(app)
119
+ register_cost_commands(app)
44
120
 
45
-
46
- @app.command("list")
47
- def list_models() -> None:
48
- """List all models and providers configuration"""
49
- config = load_config()
50
- if config is None:
51
- raise typer.Exit(1)
52
-
53
- # Auto-detect theme when not explicitly set in config, to match other CLI entrypoints.
54
- if config.theme is None:
55
- detected = is_light_terminal_background()
56
- if detected is True:
57
- config.theme = "light"
58
- elif detected is False:
59
- config.theme = "dark"
60
-
61
- display_models_and_providers(config)
62
-
63
-
64
- @app.command("config")
65
- @app.command("conf", hidden=True)
66
- def edit_config() -> None:
67
- """Open the configuration file in $EDITOR or default system editor"""
68
- editor = os.environ.get("EDITOR")
69
-
70
- # If no EDITOR is set, prioritize TextEdit on macOS
71
- if not editor:
72
- # Try common editors in order of preference on other platforms
73
- for cmd in [
74
- "code",
75
- "nvim",
76
- "vim",
77
- "nano",
78
- ]:
79
- try:
80
- subprocess.run(["which", cmd], check=True, capture_output=True)
81
- editor = cmd
82
- break
83
- except (subprocess.CalledProcessError, FileNotFoundError):
84
- continue
85
-
86
- # If no editor found, try platform-specific defaults
87
- if not editor:
88
- if sys.platform == "darwin": # macOS
89
- editor = "open"
90
- elif sys.platform == "win32": # Windows
91
- editor = "notepad"
92
- else: # Linux and other Unix systems
93
- editor = "xdg-open"
94
-
95
- # Ensure config file exists
96
- config = load_config()
97
- if config is None:
98
- raise typer.Exit(1)
99
-
100
- try:
101
- if editor == "open -a TextEdit":
102
- subprocess.run(["open", "-a", "TextEdit", str(config_path)], check=True)
103
- elif editor in ["open", "xdg-open"]:
104
- # For open/xdg-open, we need to pass the file directly
105
- subprocess.run([editor, str(config_path)], check=True)
106
- else:
107
- subprocess.run([editor, str(config_path)], check=True)
108
- except subprocess.CalledProcessError as e:
109
- log((f"Error: Failed to open editor: {e}", "red"))
110
- raise typer.Exit(1)
111
- except FileNotFoundError:
112
- log((f"Error: Editor '{editor}' not found", "red"))
113
- log("Please install a text editor or set your $EDITOR environment variable")
114
- raise typer.Exit(1)
121
+ register_self_update_commands(app)
115
122
 
116
123
 
117
124
  @app.command("exec")
@@ -156,42 +163,37 @@ def exec_command(
156
163
  ),
157
164
  ) -> None:
158
165
  """Execute non-interactively with provided input."""
166
+ update_terminal_title()
159
167
 
160
- # Set terminal title with current folder name
161
- folder_name = os.path.basename(os.getcwd())
162
- set_terminal_title(f"{folder_name}: klaude")
163
-
164
- parts: list[str] = []
165
-
166
- # Handle stdin input
167
- if not sys.stdin.isatty():
168
- try:
169
- stdin = sys.stdin.read().rstrip("\n")
170
- if stdin:
171
- parts.append(stdin)
172
- except Exception as e:
173
- log((f"Error reading from stdin: {e}", "red"))
174
-
175
- if input_content:
176
- parts.append(input_content)
177
-
178
- input_content = "\n".join(parts)
179
- if len(input_content) == 0:
180
- log(("Error: No input content provided", "red"))
168
+ merged_input = read_input_content(input_content)
169
+ if merged_input is None:
181
170
  raise typer.Exit(1)
182
171
 
172
+ from klaude_code.cli.runtime import AppInitConfig, run_exec
173
+ from klaude_code.command.model_select import select_model_interactive
174
+ from klaude_code.config import load_config
175
+
183
176
  chosen_model = model
184
- if select_model:
185
- # Prefer the explicitly provided model as default; otherwise main model
186
- config = load_config()
187
- if config is None:
188
- raise typer.Exit(1)
189
- default_name = model or config.main_model
190
- chosen_model = select_model_from_config(preferred=default_name)
177
+ if model or select_model:
178
+ chosen_model = select_model_interactive(preferred=model)
191
179
  if chosen_model is None:
192
- return
180
+ raise typer.Exit(1)
181
+ else:
182
+ # Check if main_model is configured; if not, trigger interactive selection
183
+ config = load_config()
184
+ if config.main_model is None:
185
+ chosen_model = select_model_interactive()
186
+ if chosen_model is None:
187
+ raise typer.Exit(1)
188
+ # Save the selection as default
189
+ config.main_model = chosen_model
190
+ from klaude_code.config.config import config_path
191
+ from klaude_code.trace import log
193
192
 
194
- debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
193
+ asyncio.run(config.save())
194
+ log(f"Saved main_model={chosen_model} to {config_path}", style="cyan")
195
+
196
+ debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
195
197
 
196
198
  init_config = AppInitConfig(
197
199
  model=chosen_model,
@@ -202,10 +204,13 @@ def exec_command(
202
204
  stream_json=stream_json,
203
205
  )
204
206
 
207
+ if log_path:
208
+ open_log_file_in_editor(log_path)
209
+
205
210
  asyncio.run(
206
211
  run_exec(
207
212
  init_config=init_config,
208
- input_content=input_content,
213
+ input_content=merged_input,
209
214
  )
210
215
  )
211
216
 
@@ -217,8 +222,9 @@ def main_callback(
217
222
  False,
218
223
  "--version",
219
224
  "-V",
225
+ "-v",
220
226
  help="Show version and exit",
221
- callback=_version_callback,
227
+ callback=version_option_callback,
222
228
  is_eager=True,
223
229
  ),
224
230
  model: str | None = typer.Option(
@@ -230,6 +236,11 @@ def main_callback(
230
236
  ),
231
237
  continue_: bool = typer.Option(False, "--continue", "-c", help="Continue from latest session"),
232
238
  resume: bool = typer.Option(False, "--resume", "-r", help="Select a session to resume for this project"),
239
+ resume_by_id: str | None = typer.Option(
240
+ None,
241
+ "--resume-by-id",
242
+ help="Resume a session by its ID (must exist)",
243
+ ),
233
244
  select_model: bool = typer.Option(
234
245
  False,
235
246
  "--select-model",
@@ -258,30 +269,115 @@ def main_callback(
258
269
  ) -> None:
259
270
  # Only run interactive mode when no subcommand is invoked
260
271
  if ctx.invoked_subcommand is None:
261
- # Set terminal title with current folder name
262
- folder_name = os.path.basename(os.getcwd())
263
- set_terminal_title(f"{folder_name}: klaude")
264
- # Interactive mode
272
+ from klaude_code.trace import log
273
+
274
+ resume_by_id_value = resume_by_id.strip() if resume_by_id is not None else None
275
+ if resume_by_id_value == "":
276
+ log(("Error: --resume-by-id cannot be empty", "red"))
277
+ raise typer.Exit(2)
278
+
279
+ if resume_by_id_value is not None and (resume or continue_):
280
+ log(("Error: --resume-by-id cannot be combined with --resume/--continue", "red"))
281
+ raise typer.Exit(2)
282
+
283
+ if resume_by_id_value is not None and not Session.exists(resume_by_id_value):
284
+ log((f"Error: session id '{resume_by_id_value}' not found for this project", "red"))
285
+ log(("Hint: run `klaude --resume` to select an existing session", "yellow"))
286
+ raise typer.Exit(2)
287
+
288
+ # In non-interactive environments, default to exec-mode behavior.
289
+ # This allows: echo "..." | klaude
290
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
291
+ if continue_ or resume or resume_by_id is not None:
292
+ log(("Error: --continue/--resume options require a TTY", "red"))
293
+ log(("Hint: use `klaude exec` for non-interactive usage", "yellow"))
294
+ raise typer.Exit(2)
295
+
296
+ exec_command(
297
+ input_content="",
298
+ model=model,
299
+ select_model=select_model,
300
+ debug=debug,
301
+ debug_filter=debug_filter,
302
+ vanilla=vanilla,
303
+ stream_json=False,
304
+ )
305
+ return
306
+
307
+ from klaude_code.cli.runtime import AppInitConfig, run_interactive
308
+ from klaude_code.command.model_select import select_model_interactive
309
+
310
+ update_terminal_title()
311
+
265
312
  chosen_model = model
266
- if select_model:
267
- chosen_model = select_model_from_config(preferred=model)
313
+ if model or select_model:
314
+ chosen_model = select_model_interactive(preferred=model)
268
315
  if chosen_model is None:
269
316
  return
270
317
 
271
318
  # Resolve session id before entering asyncio loop
319
+ # session_id=None means create a new session
272
320
  session_id: str | None = None
321
+
273
322
  if resume:
274
- session_id = resume_select_session()
323
+ session_id = select_session_sync()
275
324
  if session_id is None:
276
325
  return
277
326
  # If user didn't pick, allow fallback to --continue
278
327
  if session_id is None and continue_:
279
328
  session_id = Session.most_recent_session_id()
280
- # If still no session_id, generate a new one for a new session
281
- if session_id is None:
282
- session_id = uuid.uuid4().hex
283
329
 
284
- debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
330
+ if resume_by_id_value is not None:
331
+ session_id = resume_by_id_value
332
+ # If still no session_id, leave as None to create a new session
333
+
334
+ if session_id is not None and chosen_model is None:
335
+ from klaude_code.config import load_config
336
+ from klaude_code.trace import log
337
+
338
+ session_meta = Session.load_meta(session_id)
339
+ cfg = load_config()
340
+
341
+ if session_meta.model_config_name:
342
+ if any(m.model_name == session_meta.model_config_name for m in cfg.iter_model_entries()):
343
+ chosen_model = session_meta.model_config_name
344
+ else:
345
+ log(
346
+ (
347
+ f"Warning: session model '{session_meta.model_config_name}' is not defined in config; falling back to default",
348
+ "yellow",
349
+ )
350
+ )
351
+
352
+ if chosen_model is None and session_meta.model_name:
353
+ raw_model = session_meta.model_name.strip()
354
+ if raw_model:
355
+ matches = [
356
+ m.model_name
357
+ for m in cfg.iter_model_entries()
358
+ if (m.model_params.model or "").strip().lower() == raw_model.lower()
359
+ ]
360
+ if len(matches) == 1:
361
+ chosen_model = matches[0]
362
+
363
+ # If still no model, check main_model; if not configured, trigger interactive selection
364
+ if chosen_model is None:
365
+ from klaude_code.config import load_config
366
+
367
+ cfg = load_config()
368
+ if cfg.main_model is None:
369
+ chosen_model = select_model_interactive()
370
+ if chosen_model is None:
371
+ raise typer.Exit(1)
372
+ # Save the selection as default
373
+ cfg.main_model = chosen_model
374
+ from klaude_code.config.config import config_path
375
+ from klaude_code.trace import log
376
+
377
+ asyncio.run(cfg.save())
378
+ log(f"Saved main_model={chosen_model} to {config_path}", style="dim")
379
+
380
+ debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
285
381
 
286
382
  init_config = AppInitConfig(
287
383
  model=chosen_model,
@@ -290,6 +386,9 @@ def main_callback(
290
386
  debug_filters=debug_filters,
291
387
  )
292
388
 
389
+ if log_path:
390
+ open_log_file_in_editor(log_path)
391
+
293
392
  asyncio.run(
294
393
  run_interactive(
295
394
  init_config=init_config,