ripperdoc 0.2.0__py3-none-any.whl → 0.2.3__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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +74 -9
- ripperdoc/cli/commands/__init__.py +4 -0
- ripperdoc/cli/commands/agents_cmd.py +30 -4
- ripperdoc/cli/commands/context_cmd.py +11 -1
- ripperdoc/cli/commands/cost_cmd.py +5 -0
- ripperdoc/cli/commands/doctor_cmd.py +208 -0
- ripperdoc/cli/commands/memory_cmd.py +202 -0
- ripperdoc/cli/commands/models_cmd.py +61 -6
- ripperdoc/cli/commands/resume_cmd.py +4 -2
- ripperdoc/cli/commands/status_cmd.py +1 -1
- ripperdoc/cli/commands/tasks_cmd.py +27 -0
- ripperdoc/cli/ui/rich_ui.py +258 -11
- ripperdoc/cli/ui/thinking_spinner.py +128 -0
- ripperdoc/core/agents.py +14 -4
- ripperdoc/core/config.py +56 -3
- ripperdoc/core/default_tools.py +16 -2
- ripperdoc/core/permissions.py +19 -0
- ripperdoc/core/providers/__init__.py +31 -0
- ripperdoc/core/providers/anthropic.py +136 -0
- ripperdoc/core/providers/base.py +187 -0
- ripperdoc/core/providers/gemini.py +172 -0
- ripperdoc/core/providers/openai.py +142 -0
- ripperdoc/core/query.py +510 -386
- ripperdoc/core/query_utils.py +578 -0
- ripperdoc/core/system_prompt.py +2 -1
- ripperdoc/core/tool.py +16 -1
- ripperdoc/sdk/client.py +12 -1
- ripperdoc/tools/background_shell.py +63 -21
- ripperdoc/tools/bash_tool.py +48 -13
- ripperdoc/tools/file_edit_tool.py +20 -0
- ripperdoc/tools/file_read_tool.py +23 -0
- ripperdoc/tools/file_write_tool.py +20 -0
- ripperdoc/tools/glob_tool.py +59 -15
- ripperdoc/tools/grep_tool.py +7 -0
- ripperdoc/tools/ls_tool.py +246 -73
- ripperdoc/tools/mcp_tools.py +32 -10
- ripperdoc/tools/multi_edit_tool.py +23 -0
- ripperdoc/tools/notebook_edit_tool.py +18 -3
- ripperdoc/tools/task_tool.py +7 -0
- ripperdoc/tools/todo_tool.py +157 -25
- ripperdoc/tools/tool_search_tool.py +17 -4
- ripperdoc/utils/file_watch.py +134 -0
- ripperdoc/utils/git_utils.py +274 -0
- ripperdoc/utils/json_utils.py +27 -0
- ripperdoc/utils/log.py +129 -29
- ripperdoc/utils/mcp.py +71 -6
- ripperdoc/utils/memory.py +12 -1
- ripperdoc/utils/message_compaction.py +22 -5
- ripperdoc/utils/messages.py +72 -17
- ripperdoc/utils/output_utils.py +34 -9
- ripperdoc/utils/permissions/path_validation_utils.py +6 -0
- ripperdoc/utils/prompt.py +17 -0
- ripperdoc/utils/safe_get_cwd.py +4 -0
- ripperdoc/utils/session_history.py +27 -9
- ripperdoc/utils/session_usage.py +7 -0
- ripperdoc/utils/shell_utils.py +159 -0
- ripperdoc/utils/todo.py +2 -2
- {ripperdoc-0.2.0.dist-info → ripperdoc-0.2.3.dist-info}/METADATA +4 -2
- ripperdoc-0.2.3.dist-info/RECORD +95 -0
- ripperdoc-0.2.0.dist-info/RECORD +0 -81
- {ripperdoc-0.2.0.dist-info → ripperdoc-0.2.3.dist-info}/WHEEL +0 -0
- {ripperdoc-0.2.0.dist-info → ripperdoc-0.2.3.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.0.dist-info → ripperdoc-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.0.dist-info → ripperdoc-0.2.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Slash command to view and edit AGENTS memory files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, List, Optional
|
|
11
|
+
|
|
12
|
+
from rich.markup import escape
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from ripperdoc.utils.memory import (
|
|
17
|
+
LOCAL_MEMORY_FILE_NAME,
|
|
18
|
+
MEMORY_FILE_NAME,
|
|
19
|
+
collect_all_memory_files,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .base import SlashCommand
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _shorten_path(path: Path, project_path: Path) -> str:
|
|
26
|
+
"""Return a short, user-friendly path."""
|
|
27
|
+
try:
|
|
28
|
+
return str(path.resolve().relative_to(project_path.resolve()))
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
home = Path.home()
|
|
33
|
+
try:
|
|
34
|
+
rel_home = path.resolve().relative_to(home)
|
|
35
|
+
return f"~/{rel_home}"
|
|
36
|
+
except Exception:
|
|
37
|
+
return str(path)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _preferred_user_memory_path() -> Path:
|
|
41
|
+
"""Pick the user-level memory path, preferring ~/.ripperdoc/AGENTS.md."""
|
|
42
|
+
home = Path.home()
|
|
43
|
+
preferred_dir = home / ".ripperdoc"
|
|
44
|
+
preferred_path = preferred_dir / MEMORY_FILE_NAME
|
|
45
|
+
fallback_path = home / MEMORY_FILE_NAME
|
|
46
|
+
|
|
47
|
+
if preferred_path.exists():
|
|
48
|
+
return preferred_path
|
|
49
|
+
if fallback_path.exists():
|
|
50
|
+
return fallback_path
|
|
51
|
+
|
|
52
|
+
preferred_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
return preferred_path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ensure_file(path: Path) -> bool:
|
|
57
|
+
"""Ensure the target file exists. Returns True if created."""
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
if path.exists():
|
|
60
|
+
return False
|
|
61
|
+
path.write_text("", encoding="utf-8")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _ensure_gitignore_entry(project_path: Path, entry: str) -> bool:
|
|
66
|
+
"""Ensure an entry exists in .gitignore. Returns True if added."""
|
|
67
|
+
gitignore_path = project_path / ".gitignore"
|
|
68
|
+
try:
|
|
69
|
+
text = ""
|
|
70
|
+
if gitignore_path.exists():
|
|
71
|
+
text = gitignore_path.read_text(encoding="utf-8", errors="ignore")
|
|
72
|
+
existing_lines = text.splitlines()
|
|
73
|
+
if entry in existing_lines:
|
|
74
|
+
return False
|
|
75
|
+
with gitignore_path.open("a", encoding="utf-8") as f:
|
|
76
|
+
if text and not text.endswith("\n"):
|
|
77
|
+
f.write("\n")
|
|
78
|
+
f.write(f"{entry}\n")
|
|
79
|
+
return True
|
|
80
|
+
except Exception:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _determine_editor_command() -> Optional[List[str]]:
|
|
85
|
+
"""Resolve the editor command from environment or common defaults."""
|
|
86
|
+
for env_var in ("VISUAL", "EDITOR"):
|
|
87
|
+
value = os.environ.get(env_var)
|
|
88
|
+
if value:
|
|
89
|
+
return shlex.split(value)
|
|
90
|
+
|
|
91
|
+
candidates = ["code", "nano", "vim", "vi"]
|
|
92
|
+
if os.name == "nt":
|
|
93
|
+
candidates.insert(0, "notepad")
|
|
94
|
+
|
|
95
|
+
for candidate in candidates:
|
|
96
|
+
if shutil.which(candidate):
|
|
97
|
+
return [candidate]
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _open_in_editor(path: Path, console: Any) -> bool:
|
|
102
|
+
"""Open the file in a text editor; returns True if an editor was launched."""
|
|
103
|
+
editor_cmd = _determine_editor_command()
|
|
104
|
+
if not editor_cmd:
|
|
105
|
+
console.print(
|
|
106
|
+
f"[yellow]No editor configured. Set $EDITOR or $VISUAL, "
|
|
107
|
+
f"or manually edit: {escape(str(path))}[/yellow]"
|
|
108
|
+
)
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
cmd = [*editor_cmd, str(path)]
|
|
112
|
+
try:
|
|
113
|
+
console.print(f"[dim]Opening with: {' '.join(editor_cmd)}[/dim]")
|
|
114
|
+
subprocess.run(cmd, check=False)
|
|
115
|
+
return True
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
console.print(f"[red]Editor command not found: {escape(editor_cmd[0])}[/red]")
|
|
118
|
+
return False
|
|
119
|
+
except Exception as exc: # pragma: no cover - best-effort logging
|
|
120
|
+
console.print(f"[red]Failed to launch editor: {escape(str(exc))}[/red]")
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _render_memory_table(console: Any, project_path: Path) -> None:
|
|
125
|
+
files = collect_all_memory_files()
|
|
126
|
+
table = Table(title="Memory files", show_header=True, header_style="bold cyan")
|
|
127
|
+
table.add_column("Type", style="bold")
|
|
128
|
+
table.add_column("Location")
|
|
129
|
+
table.add_column("Nested", justify="center")
|
|
130
|
+
|
|
131
|
+
for memory_file in files:
|
|
132
|
+
display_path = _shorten_path(Path(memory_file.path), project_path)
|
|
133
|
+
nested = "yes" if getattr(memory_file, "is_nested", False) else ""
|
|
134
|
+
table.add_row(memory_file.type, escape(display_path), nested)
|
|
135
|
+
|
|
136
|
+
if files:
|
|
137
|
+
console.print(table)
|
|
138
|
+
else:
|
|
139
|
+
console.print("[yellow]No memory files found yet.[/yellow]")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
143
|
+
project_path = getattr(ui, "project_path", Path.cwd())
|
|
144
|
+
scope = trimmed_arg.strip().lower()
|
|
145
|
+
|
|
146
|
+
if scope:
|
|
147
|
+
scope_aliases = {
|
|
148
|
+
"project": "project",
|
|
149
|
+
"workspace": "project",
|
|
150
|
+
"local": "local",
|
|
151
|
+
"private": "local",
|
|
152
|
+
"user": "user",
|
|
153
|
+
"global": "user",
|
|
154
|
+
}
|
|
155
|
+
if scope not in scope_aliases:
|
|
156
|
+
ui.console.print("[red]Unknown scope. Use one of: project, local, user.[/red]")
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
resolved_scope = scope_aliases[scope]
|
|
160
|
+
if resolved_scope == "project":
|
|
161
|
+
target_path = project_path / MEMORY_FILE_NAME
|
|
162
|
+
heading = "Project memory (checked in)"
|
|
163
|
+
elif resolved_scope == "local":
|
|
164
|
+
target_path = project_path / LOCAL_MEMORY_FILE_NAME
|
|
165
|
+
heading = "Local memory (not checked in)"
|
|
166
|
+
else:
|
|
167
|
+
target_path = _preferred_user_memory_path()
|
|
168
|
+
heading = "User memory (home directory)"
|
|
169
|
+
|
|
170
|
+
created = _ensure_file(target_path)
|
|
171
|
+
gitignore_added = False
|
|
172
|
+
if resolved_scope == "local":
|
|
173
|
+
gitignore_added = _ensure_gitignore_entry(project_path, LOCAL_MEMORY_FILE_NAME)
|
|
174
|
+
|
|
175
|
+
_open_in_editor(target_path, ui.console)
|
|
176
|
+
|
|
177
|
+
messages: List[str] = [f"{heading}: {escape(_shorten_path(target_path, project_path))}"]
|
|
178
|
+
if created:
|
|
179
|
+
messages.append("Created new memory file.")
|
|
180
|
+
if gitignore_added:
|
|
181
|
+
messages.append("Added AGENTS.local.md to .gitignore.")
|
|
182
|
+
if not created:
|
|
183
|
+
messages.append("Opened existing memory file.")
|
|
184
|
+
|
|
185
|
+
ui.console.print(Panel("\n".join(messages), title="/memory"))
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
_render_memory_table(ui.console, project_path)
|
|
189
|
+
ui.console.print("[dim]Usage: /memory project | /memory local | /memory user[/dim]")
|
|
190
|
+
ui.console.print("[dim]Project and user memories feed directly into the system prompt.[/dim]")
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
command = SlashCommand(
|
|
195
|
+
name="memory",
|
|
196
|
+
description="List and edit AGENTS memory files",
|
|
197
|
+
handler=_handle,
|
|
198
|
+
aliases=("mem",),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = ["command"]
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
from getpass import getpass
|
|
3
|
-
from typing import Optional
|
|
1
|
+
from typing import Any, Optional
|
|
4
2
|
|
|
5
3
|
from rich.markup import escape
|
|
6
4
|
|
|
@@ -13,15 +11,23 @@ from ripperdoc.core.config import (
|
|
|
13
11
|
get_global_config,
|
|
14
12
|
set_model_pointer,
|
|
15
13
|
)
|
|
14
|
+
from ripperdoc.utils.log import get_logger
|
|
15
|
+
from ripperdoc.utils.prompt import prompt_secret
|
|
16
16
|
|
|
17
17
|
from .base import SlashCommand
|
|
18
18
|
|
|
19
|
+
logger = get_logger()
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
21
23
|
console = ui.console
|
|
22
24
|
tokens = trimmed_arg.split()
|
|
23
25
|
subcmd = tokens[0].lower() if tokens else ""
|
|
24
26
|
config = get_global_config()
|
|
27
|
+
logger.info(
|
|
28
|
+
"[models_cmd] Handling /models command",
|
|
29
|
+
extra={"subcommand": subcmd or "list", "session_id": getattr(ui, "session_id", None)},
|
|
30
|
+
)
|
|
25
31
|
|
|
26
32
|
def print_models_usage() -> None:
|
|
27
33
|
console.print("[bold]/models[/bold] — list configured models")
|
|
@@ -103,9 +109,18 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
103
109
|
console.print("[red]Model name is required.[/red]")
|
|
104
110
|
return True
|
|
105
111
|
|
|
106
|
-
api_key_input =
|
|
112
|
+
api_key_input = prompt_secret("API key (leave blank to keep unset)").strip()
|
|
107
113
|
api_key = api_key_input or (existing_profile.api_key if existing_profile else None)
|
|
108
114
|
|
|
115
|
+
auth_token = existing_profile.auth_token if existing_profile else None
|
|
116
|
+
if provider == ProviderType.ANTHROPIC:
|
|
117
|
+
auth_token_input = prompt_secret(
|
|
118
|
+
"Auth token (Anthropic only, leave blank to keep unset)"
|
|
119
|
+
).strip()
|
|
120
|
+
auth_token = auth_token_input or auth_token
|
|
121
|
+
else:
|
|
122
|
+
auth_token = None
|
|
123
|
+
|
|
109
124
|
api_base_default = existing_profile.api_base if existing_profile else ""
|
|
110
125
|
api_base = (
|
|
111
126
|
console.input(
|
|
@@ -156,6 +171,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
156
171
|
max_tokens=max_tokens,
|
|
157
172
|
temperature=temperature,
|
|
158
173
|
context_window=context_window,
|
|
174
|
+
auth_token=auth_token,
|
|
159
175
|
)
|
|
160
176
|
|
|
161
177
|
try:
|
|
@@ -167,6 +183,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
167
183
|
)
|
|
168
184
|
except Exception as exc:
|
|
169
185
|
console.print(f"[red]Failed to save model: {escape(str(exc))}[/red]")
|
|
186
|
+
logger.exception(
|
|
187
|
+
"[models_cmd] Failed to save model profile",
|
|
188
|
+
extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
|
|
189
|
+
)
|
|
170
190
|
return True
|
|
171
191
|
|
|
172
192
|
marker = " (main)" if set_as_main else ""
|
|
@@ -202,8 +222,8 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
202
222
|
)
|
|
203
223
|
|
|
204
224
|
api_key_label = "[set]" if existing_profile.api_key else "[not set]"
|
|
205
|
-
api_key_prompt = f"API key {api_key_label} (Enter=keep, '-'=clear)
|
|
206
|
-
api_key_input =
|
|
225
|
+
api_key_prompt = f"API key {api_key_label} (Enter=keep, '-'=clear)"
|
|
226
|
+
api_key_input = prompt_secret(api_key_prompt).strip()
|
|
207
227
|
if api_key_input == "-":
|
|
208
228
|
api_key = None
|
|
209
229
|
elif api_key_input:
|
|
@@ -211,6 +231,21 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
211
231
|
else:
|
|
212
232
|
api_key = existing_profile.api_key
|
|
213
233
|
|
|
234
|
+
auth_token = existing_profile.auth_token
|
|
235
|
+
if (
|
|
236
|
+
provider == ProviderType.ANTHROPIC
|
|
237
|
+
or existing_profile.provider == ProviderType.ANTHROPIC
|
|
238
|
+
):
|
|
239
|
+
auth_label = "[set]" if auth_token else "[not set]"
|
|
240
|
+
auth_prompt = f"Auth token (Anthropic only) {auth_label} (Enter=keep, '-'=clear)"
|
|
241
|
+
auth_token_input = prompt_secret(auth_prompt).strip()
|
|
242
|
+
if auth_token_input == "-":
|
|
243
|
+
auth_token = None
|
|
244
|
+
elif auth_token_input:
|
|
245
|
+
auth_token = auth_token_input
|
|
246
|
+
else:
|
|
247
|
+
auth_token = None
|
|
248
|
+
|
|
214
249
|
api_base = (
|
|
215
250
|
console.input(f"API base (optional) [{existing_profile.api_base or ''}]: ").strip()
|
|
216
251
|
or existing_profile.api_base
|
|
@@ -244,6 +279,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
244
279
|
max_tokens=max_tokens,
|
|
245
280
|
temperature=temperature,
|
|
246
281
|
context_window=context_window,
|
|
282
|
+
auth_token=auth_token,
|
|
247
283
|
)
|
|
248
284
|
|
|
249
285
|
try:
|
|
@@ -255,6 +291,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
255
291
|
)
|
|
256
292
|
except Exception as exc:
|
|
257
293
|
console.print(f"[red]Failed to update model: {escape(str(exc))}[/red]")
|
|
294
|
+
logger.exception(
|
|
295
|
+
"[models_cmd] Failed to update model profile",
|
|
296
|
+
extra={"profile": profile_name, "session_id": getattr(ui, "session_id", None)},
|
|
297
|
+
)
|
|
258
298
|
return True
|
|
259
299
|
|
|
260
300
|
console.print(f"[green]✓ Model '{escape(profile_name)}' updated[/green]")
|
|
@@ -274,6 +314,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
274
314
|
except Exception as exc:
|
|
275
315
|
console.print(f"[red]Failed to delete model: {escape(str(exc))}[/red]")
|
|
276
316
|
print_models_usage()
|
|
317
|
+
logger.exception(
|
|
318
|
+
"[models_cmd] Failed to delete model profile",
|
|
319
|
+
extra={"profile": target, "session_id": getattr(ui, "session_id", None)},
|
|
320
|
+
)
|
|
277
321
|
return True
|
|
278
322
|
|
|
279
323
|
if subcmd in ("use", "main", "set-main"):
|
|
@@ -288,6 +332,10 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
288
332
|
except Exception as exc:
|
|
289
333
|
console.print(f"[red]{escape(str(exc))}[/red]")
|
|
290
334
|
print_models_usage()
|
|
335
|
+
logger.exception(
|
|
336
|
+
"[models_cmd] Failed to set main model pointer",
|
|
337
|
+
extra={"profile": target, "session_id": getattr(ui, "session_id", None)},
|
|
338
|
+
)
|
|
291
339
|
return True
|
|
292
340
|
|
|
293
341
|
print_models_usage()
|
|
@@ -312,6 +360,13 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
312
360
|
markup=False,
|
|
313
361
|
)
|
|
314
362
|
console.print(f" api_key: {'***' if profile.api_key else 'Not set'}", markup=False)
|
|
363
|
+
if profile.provider == ProviderType.ANTHROPIC:
|
|
364
|
+
console.print(
|
|
365
|
+
f" auth_token: {'***' if getattr(profile, 'auth_token', None) else 'Not set'}",
|
|
366
|
+
markup=False,
|
|
367
|
+
)
|
|
368
|
+
if profile.openai_tool_mode:
|
|
369
|
+
console.print(f" openai_tool_mode: {profile.openai_tool_mode}", markup=False)
|
|
315
370
|
pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
|
|
316
371
|
console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
|
|
317
372
|
return True
|
|
@@ -30,7 +30,8 @@ def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
|
|
|
30
30
|
if 0 <= idx < len(sessions):
|
|
31
31
|
return sessions[idx]
|
|
32
32
|
ui.console.print(
|
|
33
|
-
f"[red]Invalid session index {escape(str(idx))}.
|
|
33
|
+
f"[red]Invalid session index {escape(str(idx))}. "
|
|
34
|
+
f"Choose 0-{len(sessions) - 1}.[/red]"
|
|
34
35
|
)
|
|
35
36
|
else:
|
|
36
37
|
# Treat arg as session id if it matches.
|
|
@@ -60,7 +61,8 @@ def _choose_session(ui: Any, arg: str) -> Optional[SessionSummary]:
|
|
|
60
61
|
idx = int(choice_text)
|
|
61
62
|
if idx < 0 or idx >= len(sessions):
|
|
62
63
|
ui.console.print(
|
|
63
|
-
f"[red]Invalid session index {escape(str(idx))}.
|
|
64
|
+
f"[red]Invalid session index {escape(str(idx))}. "
|
|
65
|
+
f"Choose 0-{len(sessions) - 1}.[/red]"
|
|
64
66
|
)
|
|
65
67
|
return None
|
|
66
68
|
return sessions[idx]
|
|
@@ -34,7 +34,7 @@ def _auth_token_display(profile: Optional[ModelProfile]) -> Tuple[str, Optional[
|
|
|
34
34
|
env_var = next((name for name in env_candidates if os.environ.get(name)), None)
|
|
35
35
|
if env_var:
|
|
36
36
|
return (f"{env_var} (env)", env_var)
|
|
37
|
-
if profile.api_key:
|
|
37
|
+
if profile.api_key or getattr(profile, "auth_token", None):
|
|
38
38
|
return ("Configured in profile", None)
|
|
39
39
|
return ("Missing", None)
|
|
40
40
|
|
|
@@ -13,11 +13,15 @@ from ripperdoc.tools.background_shell import (
|
|
|
13
13
|
kill_background_task,
|
|
14
14
|
list_background_tasks,
|
|
15
15
|
)
|
|
16
|
+
from ripperdoc.utils.log import get_logger
|
|
16
17
|
|
|
17
18
|
from typing import Any, Optional
|
|
18
19
|
from .base import SlashCommand
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
logger = get_logger()
|
|
23
|
+
|
|
24
|
+
|
|
21
25
|
def _format_duration(duration_ms: Optional[float]) -> str:
|
|
22
26
|
"""Render milliseconds into a short human-readable duration."""
|
|
23
27
|
if duration_ms is None:
|
|
@@ -103,6 +107,10 @@ def _list_tasks(ui: Any) -> bool:
|
|
|
103
107
|
status = get_background_status(task_id, consume=False)
|
|
104
108
|
except Exception as exc:
|
|
105
109
|
table.add_row(escape(task_id), "[red]error[/]", escape(str(exc)), "-")
|
|
110
|
+
logger.exception(
|
|
111
|
+
"[tasks_cmd] Failed to read background task status",
|
|
112
|
+
extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
|
|
113
|
+
)
|
|
106
114
|
continue
|
|
107
115
|
|
|
108
116
|
command = status.get("command") or ""
|
|
@@ -137,6 +145,10 @@ def _kill_task(ui: Any, task_id: str) -> bool:
|
|
|
137
145
|
return True
|
|
138
146
|
except Exception as exc:
|
|
139
147
|
console.print(f"[red]Failed to read task '{escape(task_id)}': {escape(str(exc))}[/red]")
|
|
148
|
+
logger.exception(
|
|
149
|
+
"[tasks_cmd] Failed to read task before kill",
|
|
150
|
+
extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
|
|
151
|
+
)
|
|
140
152
|
return True
|
|
141
153
|
|
|
142
154
|
if status.get("status") != "running":
|
|
@@ -149,6 +161,10 @@ def _kill_task(ui: Any, task_id: str) -> bool:
|
|
|
149
161
|
killed = asyncio.run(kill_background_task(task_id))
|
|
150
162
|
except Exception as exc:
|
|
151
163
|
console.print(f"[red]Error stopping task {escape(task_id)}: {escape(str(exc))}[/red]")
|
|
164
|
+
logger.exception(
|
|
165
|
+
"[tasks_cmd] Error stopping background task",
|
|
166
|
+
extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
|
|
167
|
+
)
|
|
152
168
|
return True
|
|
153
169
|
|
|
154
170
|
if killed:
|
|
@@ -169,6 +185,10 @@ def _show_task(ui: Any, task_id: str) -> bool:
|
|
|
169
185
|
return True
|
|
170
186
|
except Exception as exc:
|
|
171
187
|
console.print(f"[red]Failed to read task '{escape(task_id)}': {escape(str(exc))}[/red]")
|
|
188
|
+
logger.exception(
|
|
189
|
+
"[tasks_cmd] Failed to read task for detail view",
|
|
190
|
+
extra={"task_id": task_id, "session_id": getattr(ui, "session_id", None)},
|
|
191
|
+
)
|
|
172
192
|
return True
|
|
173
193
|
|
|
174
194
|
details = Table(box=box.SIMPLE_HEAVY, show_header=False)
|
|
@@ -208,6 +228,13 @@ def _show_task(ui: Any, task_id: str) -> bool:
|
|
|
208
228
|
|
|
209
229
|
def _handle(ui: Any, args: str) -> bool:
|
|
210
230
|
parts = args.split()
|
|
231
|
+
logger.info(
|
|
232
|
+
"[tasks_cmd] Handling /tasks command",
|
|
233
|
+
extra={
|
|
234
|
+
"session_id": getattr(ui, "session_id", None),
|
|
235
|
+
"raw_args": args,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
211
238
|
if not parts:
|
|
212
239
|
return _list_tasks(ui)
|
|
213
240
|
|