ripperdoc 0.2.10__py3-none-any.whl → 0.3.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.
- ripperdoc/__init__.py +1 -1
- ripperdoc/cli/cli.py +164 -57
- ripperdoc/cli/commands/__init__.py +4 -0
- ripperdoc/cli/commands/agents_cmd.py +3 -7
- ripperdoc/cli/commands/doctor_cmd.py +29 -0
- ripperdoc/cli/commands/memory_cmd.py +2 -1
- ripperdoc/cli/commands/models_cmd.py +61 -5
- ripperdoc/cli/commands/resume_cmd.py +1 -0
- ripperdoc/cli/commands/skills_cmd.py +103 -0
- ripperdoc/cli/commands/stats_cmd.py +4 -4
- ripperdoc/cli/commands/status_cmd.py +10 -0
- ripperdoc/cli/commands/tasks_cmd.py +6 -3
- ripperdoc/cli/commands/themes_cmd.py +139 -0
- ripperdoc/cli/ui/file_mention_completer.py +63 -13
- ripperdoc/cli/ui/helpers.py +6 -3
- ripperdoc/cli/ui/interrupt_handler.py +34 -0
- ripperdoc/cli/ui/panels.py +13 -8
- ripperdoc/cli/ui/rich_ui.py +451 -32
- ripperdoc/cli/ui/spinner.py +68 -5
- ripperdoc/cli/ui/tool_renderers.py +10 -9
- ripperdoc/cli/ui/wizard.py +18 -11
- ripperdoc/core/agents.py +4 -0
- ripperdoc/core/config.py +235 -0
- ripperdoc/core/default_tools.py +1 -0
- ripperdoc/core/hooks/llm_callback.py +0 -1
- ripperdoc/core/hooks/manager.py +6 -0
- ripperdoc/core/permissions.py +82 -5
- ripperdoc/core/providers/openai.py +55 -9
- ripperdoc/core/query.py +349 -108
- ripperdoc/core/query_utils.py +17 -14
- ripperdoc/core/skills.py +1 -0
- ripperdoc/core/theme.py +298 -0
- ripperdoc/core/tool.py +8 -3
- ripperdoc/protocol/__init__.py +14 -0
- ripperdoc/protocol/models.py +300 -0
- ripperdoc/protocol/stdio.py +1453 -0
- ripperdoc/tools/background_shell.py +49 -5
- ripperdoc/tools/bash_tool.py +75 -9
- ripperdoc/tools/file_edit_tool.py +98 -29
- ripperdoc/tools/file_read_tool.py +139 -8
- ripperdoc/tools/file_write_tool.py +46 -3
- ripperdoc/tools/grep_tool.py +98 -8
- ripperdoc/tools/lsp_tool.py +9 -15
- ripperdoc/tools/multi_edit_tool.py +26 -3
- ripperdoc/tools/skill_tool.py +52 -1
- ripperdoc/tools/task_tool.py +33 -8
- ripperdoc/utils/file_watch.py +12 -6
- ripperdoc/utils/image_utils.py +125 -0
- ripperdoc/utils/log.py +30 -3
- ripperdoc/utils/lsp.py +9 -3
- ripperdoc/utils/mcp.py +80 -18
- ripperdoc/utils/message_formatting.py +2 -2
- ripperdoc/utils/messages.py +177 -32
- ripperdoc/utils/pending_messages.py +50 -0
- ripperdoc/utils/permissions/shell_command_validation.py +3 -3
- ripperdoc/utils/permissions/tool_permission_utils.py +9 -3
- ripperdoc/utils/platform.py +198 -0
- ripperdoc/utils/session_heatmap.py +1 -3
- ripperdoc/utils/session_history.py +2 -2
- ripperdoc/utils/session_stats.py +1 -0
- ripperdoc/utils/shell_utils.py +8 -5
- ripperdoc/utils/todo.py +0 -6
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/METADATA +49 -17
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/RECORD +68 -61
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/WHEEL +1 -1
- ripperdoc/sdk/__init__.py +0 -9
- ripperdoc/sdk/client.py +0 -408
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/entry_points.txt +0 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {ripperdoc-0.2.10.dist-info → ripperdoc-0.3.0.dist-info}/top_level.txt +0 -0
ripperdoc/__init__.py
CHANGED
ripperdoc/cli/cli.py
CHANGED
|
@@ -44,7 +44,6 @@ from ripperdoc.utils.log import enable_session_file_logging, get_logger
|
|
|
44
44
|
|
|
45
45
|
from rich.console import Console
|
|
46
46
|
from rich.markdown import Markdown
|
|
47
|
-
from rich.panel import Panel
|
|
48
47
|
from rich.markup import escape
|
|
49
48
|
|
|
50
49
|
console = Console()
|
|
@@ -98,7 +97,6 @@ async def run_query(
|
|
|
98
97
|
model: Optional[str] = None,
|
|
99
98
|
) -> None:
|
|
100
99
|
"""Run a single query and print the response."""
|
|
101
|
-
|
|
102
100
|
logger.info(
|
|
103
101
|
"[cli] Running single prompt session",
|
|
104
102
|
extra={
|
|
@@ -212,55 +210,37 @@ async def run_query(
|
|
|
212
210
|
mcp_instructions=mcp_instructions,
|
|
213
211
|
)
|
|
214
212
|
|
|
215
|
-
# Run the query
|
|
213
|
+
# Run the query - collect final response text
|
|
214
|
+
final_response_parts: List[str] = []
|
|
216
215
|
try:
|
|
217
216
|
async for message in query(
|
|
218
217
|
messages, system_prompt, context, query_context, can_use_tool
|
|
219
218
|
):
|
|
220
219
|
if message.type == "assistant" and hasattr(message, "message"):
|
|
221
|
-
#
|
|
220
|
+
# Collect assistant message text for final output
|
|
222
221
|
if isinstance(message.message.content, str):
|
|
223
|
-
|
|
224
|
-
Panel(
|
|
225
|
-
Markdown(message.message.content),
|
|
226
|
-
title="Ripperdoc",
|
|
227
|
-
border_style="cyan",
|
|
228
|
-
padding=(0, 1),
|
|
229
|
-
)
|
|
230
|
-
)
|
|
222
|
+
final_response_parts.append(message.message.content)
|
|
231
223
|
else:
|
|
232
224
|
# Handle structured content
|
|
233
225
|
for block in message.message.content:
|
|
234
226
|
if isinstance(block, dict):
|
|
235
227
|
if block.get("type") == "text":
|
|
236
|
-
|
|
237
|
-
Panel(
|
|
238
|
-
Markdown(block["text"]),
|
|
239
|
-
title="Ripperdoc",
|
|
240
|
-
border_style="cyan",
|
|
241
|
-
padding=(0, 1),
|
|
242
|
-
)
|
|
243
|
-
)
|
|
228
|
+
final_response_parts.append(block["text"])
|
|
244
229
|
else:
|
|
245
230
|
if hasattr(block, "type") and block.type == "text":
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
title="Ripperdoc",
|
|
250
|
-
border_style="cyan",
|
|
251
|
-
padding=(0, 1),
|
|
252
|
-
)
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
elif message.type == "progress" and hasattr(message, "content"):
|
|
256
|
-
# Print progress
|
|
257
|
-
if verbose:
|
|
258
|
-
console.print(f"[dim]Progress: {escape(str(message.content))}[/dim]")
|
|
231
|
+
final_response_parts.append(block.text or "")
|
|
232
|
+
|
|
233
|
+
# Skip progress messages entirely for -p mode
|
|
259
234
|
|
|
260
235
|
# Add message to history
|
|
261
236
|
messages.append(message) # type: ignore[arg-type]
|
|
262
237
|
session_history.append(message) # type: ignore[arg-type]
|
|
263
238
|
|
|
239
|
+
# Print final response as clean markdown (no panel, no decoration)
|
|
240
|
+
if final_response_parts:
|
|
241
|
+
final_text = "\n".join(final_response_parts)
|
|
242
|
+
console.print(Markdown(final_text))
|
|
243
|
+
|
|
264
244
|
except KeyboardInterrupt:
|
|
265
245
|
console.print("\n[yellow]Interrupted by user[/yellow]")
|
|
266
246
|
except asyncio.CancelledError:
|
|
@@ -308,7 +288,6 @@ async def run_query(
|
|
|
308
288
|
logger.debug("[cli] Shutdown MCP runtime", extra={"session_id": session_id})
|
|
309
289
|
|
|
310
290
|
|
|
311
|
-
|
|
312
291
|
@click.group(invoke_without_command=True)
|
|
313
292
|
@click.version_option(version=__version__)
|
|
314
293
|
@click.option("--cwd", type=click.Path(exists=True), help="Working directory")
|
|
@@ -374,19 +353,51 @@ def cli(
|
|
|
374
353
|
) -> None:
|
|
375
354
|
"""Ripperdoc - AI-powered coding agent"""
|
|
376
355
|
session_id = str(uuid.uuid4())
|
|
356
|
+
cwd_changed: Optional[str] = None
|
|
377
357
|
|
|
378
358
|
# Set working directory
|
|
379
359
|
if cwd:
|
|
380
360
|
import os
|
|
381
361
|
|
|
382
362
|
os.chdir(cwd)
|
|
363
|
+
cwd_changed = cwd
|
|
364
|
+
|
|
365
|
+
project_path = Path.cwd()
|
|
366
|
+
|
|
367
|
+
# Handle --continue option: load the most recent session
|
|
368
|
+
resume_messages = None
|
|
369
|
+
most_recent = None
|
|
370
|
+
if continue_session:
|
|
371
|
+
summaries = list_session_summaries(project_path)
|
|
372
|
+
if summaries:
|
|
373
|
+
most_recent = summaries[0]
|
|
374
|
+
session_id = most_recent.session_id
|
|
375
|
+
resume_messages = load_session_messages(project_path, session_id)
|
|
376
|
+
console.print(f"[dim]Continuing session: {most_recent.last_prompt}[/dim]")
|
|
377
|
+
else:
|
|
378
|
+
console.print("[yellow]No previous sessions found in this directory.[/yellow]")
|
|
379
|
+
|
|
380
|
+
log_file = enable_session_file_logging(project_path, session_id)
|
|
381
|
+
|
|
382
|
+
if cwd_changed:
|
|
383
383
|
logger.debug(
|
|
384
384
|
"[cli] Changed working directory via --cwd",
|
|
385
|
-
extra={"cwd":
|
|
385
|
+
extra={"cwd": cwd_changed, "session_id": session_id},
|
|
386
386
|
)
|
|
387
387
|
|
|
388
|
-
|
|
389
|
-
|
|
388
|
+
if most_recent:
|
|
389
|
+
logger.info(
|
|
390
|
+
"[cli] Continuing session",
|
|
391
|
+
extra={
|
|
392
|
+
"session_id": session_id,
|
|
393
|
+
"message_count": len(resume_messages) if resume_messages else 0,
|
|
394
|
+
"last_prompt": most_recent.last_prompt,
|
|
395
|
+
"log_file": str(log_file),
|
|
396
|
+
},
|
|
397
|
+
)
|
|
398
|
+
elif continue_session:
|
|
399
|
+
logger.warning("[cli] No previous sessions found to continue")
|
|
400
|
+
|
|
390
401
|
logger.info(
|
|
391
402
|
"[cli] Starting CLI invocation",
|
|
392
403
|
extra={
|
|
@@ -412,26 +423,39 @@ def cli(
|
|
|
412
423
|
# Parse --tools option
|
|
413
424
|
allowed_tools = parse_tools_option(tools)
|
|
414
425
|
|
|
415
|
-
# Handle
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
426
|
+
# Handle piped stdin input
|
|
427
|
+
# - With -p flag: Not applicable (prompt comes from -p argument)
|
|
428
|
+
# - Without -p: stdin becomes the initial query in interactive mode
|
|
429
|
+
initial_query: Optional[str] = None
|
|
430
|
+
if prompt is None and ctx.invoked_subcommand is None:
|
|
431
|
+
stdin_stream = click.get_text_stream("stdin")
|
|
432
|
+
try:
|
|
433
|
+
stdin_is_tty = stdin_stream.isatty()
|
|
434
|
+
except Exception:
|
|
435
|
+
stdin_is_tty = True
|
|
436
|
+
|
|
437
|
+
if not stdin_is_tty:
|
|
438
|
+
try:
|
|
439
|
+
stdin_data = stdin_stream.read()
|
|
440
|
+
except (OSError, ValueError) as exc:
|
|
441
|
+
logger.warning(
|
|
442
|
+
"[cli] Failed to read stdin for initial query: %s: %s",
|
|
443
|
+
type(exc).__name__,
|
|
444
|
+
exc,
|
|
445
|
+
extra={"session_id": session_id},
|
|
446
|
+
)
|
|
447
|
+
else:
|
|
448
|
+
trimmed = stdin_data.rstrip("\n")
|
|
449
|
+
if trimmed.strip():
|
|
450
|
+
initial_query = trimmed
|
|
451
|
+
logger.info(
|
|
452
|
+
"[cli] Received initial query from stdin",
|
|
453
|
+
extra={
|
|
454
|
+
"session_id": session_id,
|
|
455
|
+
"query_length": len(initial_query),
|
|
456
|
+
"query_preview": initial_query[:200],
|
|
457
|
+
},
|
|
458
|
+
)
|
|
435
459
|
|
|
436
460
|
logger.debug(
|
|
437
461
|
"[cli] Configuration initialized",
|
|
@@ -480,6 +504,7 @@ def cli(
|
|
|
480
504
|
append_system_prompt=append_system_prompt,
|
|
481
505
|
model=model,
|
|
482
506
|
resume_messages=resume_messages,
|
|
507
|
+
initial_query=initial_query,
|
|
483
508
|
)
|
|
484
509
|
return
|
|
485
510
|
|
|
@@ -507,6 +532,88 @@ def config_cmd() -> None:
|
|
|
507
532
|
console.print()
|
|
508
533
|
|
|
509
534
|
|
|
535
|
+
@cli.command(name="stdio")
|
|
536
|
+
@click.option(
|
|
537
|
+
"--input-format",
|
|
538
|
+
type=click.Choice(["stream-json", "auto"]),
|
|
539
|
+
default="stream-json",
|
|
540
|
+
help="Input format for messages.",
|
|
541
|
+
)
|
|
542
|
+
@click.option(
|
|
543
|
+
"--output-format",
|
|
544
|
+
type=click.Choice(["stream-json"]),
|
|
545
|
+
default="stream-json",
|
|
546
|
+
help="Output format for messages.",
|
|
547
|
+
)
|
|
548
|
+
@click.option(
|
|
549
|
+
"--model",
|
|
550
|
+
type=str,
|
|
551
|
+
default=None,
|
|
552
|
+
help="Model profile for the current session.",
|
|
553
|
+
)
|
|
554
|
+
@click.option(
|
|
555
|
+
"--permission-mode",
|
|
556
|
+
type=click.Choice(["default", "acceptEdits", "plan", "bypassPermissions"]),
|
|
557
|
+
default="default",
|
|
558
|
+
help="Permission mode for tool usage.",
|
|
559
|
+
)
|
|
560
|
+
@click.option(
|
|
561
|
+
"--max-turns",
|
|
562
|
+
type=int,
|
|
563
|
+
default=None,
|
|
564
|
+
help="Maximum number of conversation turns.",
|
|
565
|
+
)
|
|
566
|
+
@click.option(
|
|
567
|
+
"--system-prompt",
|
|
568
|
+
type=str,
|
|
569
|
+
default=None,
|
|
570
|
+
help="System prompt to use for the session.",
|
|
571
|
+
)
|
|
572
|
+
@click.option(
|
|
573
|
+
"--print",
|
|
574
|
+
"-p",
|
|
575
|
+
is_flag=True,
|
|
576
|
+
help="Print mode (for single prompt queries).",
|
|
577
|
+
)
|
|
578
|
+
@click.option(
|
|
579
|
+
"--",
|
|
580
|
+
"prompt",
|
|
581
|
+
type=str,
|
|
582
|
+
default=None,
|
|
583
|
+
help="Direct prompt (for print mode).",
|
|
584
|
+
)
|
|
585
|
+
def stdio_cmd(
|
|
586
|
+
input_format: str,
|
|
587
|
+
output_format: str,
|
|
588
|
+
model: str | None,
|
|
589
|
+
permission_mode: str,
|
|
590
|
+
max_turns: int | None,
|
|
591
|
+
system_prompt: str | None,
|
|
592
|
+
print: bool,
|
|
593
|
+
prompt: str | None,
|
|
594
|
+
) -> None:
|
|
595
|
+
"""Stdio mode for SDK subprocess communication.
|
|
596
|
+
|
|
597
|
+
This command enables Ripperdoc to communicate with SDKs via JSON Control
|
|
598
|
+
Protocol over stdin/stdout.
|
|
599
|
+
"""
|
|
600
|
+
from ripperdoc.protocol.stdio import _run_stdio
|
|
601
|
+
import asyncio
|
|
602
|
+
|
|
603
|
+
asyncio.run(
|
|
604
|
+
_run_stdio(
|
|
605
|
+
input_format=input_format,
|
|
606
|
+
output_format=output_format,
|
|
607
|
+
model=model,
|
|
608
|
+
permission_mode=permission_mode,
|
|
609
|
+
max_turns=max_turns,
|
|
610
|
+
system_prompt=system_prompt,
|
|
611
|
+
print_mode=print,
|
|
612
|
+
prompt=prompt,
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
|
|
510
617
|
@cli.command(name="version")
|
|
511
618
|
def version_cmd() -> None:
|
|
512
619
|
"""Show version information"""
|
|
@@ -21,9 +21,11 @@ from .mcp_cmd import command as mcp_command
|
|
|
21
21
|
from .models_cmd import command as models_command
|
|
22
22
|
from .permissions_cmd import command as permissions_command
|
|
23
23
|
from .resume_cmd import command as resume_command
|
|
24
|
+
from .skills_cmd import command as skills_command
|
|
24
25
|
from .stats_cmd import command as stats_command
|
|
25
26
|
from .tasks_cmd import command as tasks_command
|
|
26
27
|
from .status_cmd import command as status_command
|
|
28
|
+
from .themes_cmd import command as themes_command
|
|
27
29
|
from .todos_cmd import command as todos_command
|
|
28
30
|
from .tools_cmd import command as tools_command
|
|
29
31
|
|
|
@@ -64,7 +66,9 @@ ALL_COMMANDS: List[SlashCommand] = [
|
|
|
64
66
|
context_command,
|
|
65
67
|
compact_command,
|
|
66
68
|
resume_command,
|
|
69
|
+
skills_command,
|
|
67
70
|
agents_command,
|
|
71
|
+
themes_command,
|
|
68
72
|
]
|
|
69
73
|
|
|
70
74
|
COMMAND_REGISTRY: Dict[str, SlashCommand] = _build_registry(ALL_COMMANDS)
|
|
@@ -18,7 +18,7 @@ from ripperdoc.tools.task_tool import (
|
|
|
18
18
|
)
|
|
19
19
|
from ripperdoc.utils.log import get_logger
|
|
20
20
|
|
|
21
|
-
from typing import Any, Dict
|
|
21
|
+
from typing import Any, Dict
|
|
22
22
|
from .base import SlashCommand
|
|
23
23
|
|
|
24
24
|
logger = get_logger()
|
|
@@ -101,9 +101,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
101
101
|
for run_id in sorted(run_ids):
|
|
102
102
|
snapshot: Dict[Any, Any] = get_agent_run_snapshot(run_id) or {}
|
|
103
103
|
result_text = snapshot.get("result_text") or snapshot.get("error") or ""
|
|
104
|
-
result_preview = (
|
|
105
|
-
result_text if len(result_text) <= 80 else result_text[:77] + "..."
|
|
106
|
-
)
|
|
104
|
+
result_preview = result_text if len(result_text) <= 80 else result_text[:77] + "..."
|
|
107
105
|
table.add_row(
|
|
108
106
|
escape(run_id),
|
|
109
107
|
escape(snapshot.get("status") or "unknown"),
|
|
@@ -136,9 +134,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
136
134
|
details.add_row("Status", escape(snapshot.get("status") or "unknown"))
|
|
137
135
|
details.add_row("Agent", escape(snapshot.get("agent_type") or "unknown"))
|
|
138
136
|
details.add_row("Duration", _format_duration(snapshot.get("duration_ms")))
|
|
139
|
-
details.add_row(
|
|
140
|
-
"Background", "yes" if snapshot.get("is_background") else "no"
|
|
141
|
-
)
|
|
137
|
+
details.add_row("Background", "yes" if snapshot.get("is_background") else "no")
|
|
142
138
|
if snapshot.get("model_used"):
|
|
143
139
|
details.add_row("Model", escape(str(snapshot.get("model_used"))))
|
|
144
140
|
if snapshot.get("tool_use_count"):
|
|
@@ -15,6 +15,8 @@ from ripperdoc.core.config import (
|
|
|
15
15
|
api_key_env_candidates,
|
|
16
16
|
get_global_config,
|
|
17
17
|
get_project_config,
|
|
18
|
+
get_ripperdoc_env_status,
|
|
19
|
+
has_ripperdoc_env_overrides,
|
|
18
20
|
)
|
|
19
21
|
from ripperdoc.cli.ui.helpers import get_profile_for_pointer
|
|
20
22
|
from ripperdoc.utils.log import get_logger
|
|
@@ -41,6 +43,12 @@ def _api_key_status(provider: ProviderType, profile_key: Optional[str]) -> Tuple
|
|
|
41
43
|
"""Check API key presence and source."""
|
|
42
44
|
import os
|
|
43
45
|
|
|
46
|
+
# 首先检查全局 RIPPERDOC_API_KEY
|
|
47
|
+
if ripperdoc_api_key := os.getenv("RIPPERDOC_API_KEY"):
|
|
48
|
+
masked = ripperdoc_api_key[:4] + "…" if len(ripperdoc_api_key) > 4 else "set"
|
|
49
|
+
return ("ok", f"Found in $RIPPERDOC_API_KEY ({masked})")
|
|
50
|
+
|
|
51
|
+
# 然后检查 provider 特定的环境变量
|
|
44
52
|
for env_var in api_key_env_candidates(provider):
|
|
45
53
|
if os.environ.get(env_var):
|
|
46
54
|
masked = os.environ[env_var]
|
|
@@ -186,6 +194,23 @@ def _project_status(project_path: Path) -> Tuple[str, str, str]:
|
|
|
186
194
|
)
|
|
187
195
|
|
|
188
196
|
|
|
197
|
+
def _ripperdoc_env_status() -> List[Tuple[str, str, str]]:
|
|
198
|
+
"""Check RIPPERDOC_* environment variable overrides."""
|
|
199
|
+
rows: List[Tuple[str, str, str]] = []
|
|
200
|
+
|
|
201
|
+
if not has_ripperdoc_env_overrides():
|
|
202
|
+
rows.append(_status_row("Env overrides", "ok", "No RIPPERDOC_* overrides active"))
|
|
203
|
+
return rows
|
|
204
|
+
|
|
205
|
+
rows.append(_status_row("Env overrides", "ok", "RIPPERDOC_* variables detected"))
|
|
206
|
+
|
|
207
|
+
status = get_ripperdoc_env_status()
|
|
208
|
+
for key, value in status.items():
|
|
209
|
+
rows.append(_status_row("", "ok", f" {key}: {value}"))
|
|
210
|
+
|
|
211
|
+
return rows
|
|
212
|
+
|
|
213
|
+
|
|
189
214
|
def _render_table(console: Any, rows: List[Tuple[str, str, str]]) -> None:
|
|
190
215
|
table = Table(show_header=True, header_style="bold cyan")
|
|
191
216
|
table.add_column("Check")
|
|
@@ -202,6 +227,10 @@ def _handle(ui: Any, _: str) -> bool:
|
|
|
202
227
|
|
|
203
228
|
results.append(_onboarding_status())
|
|
204
229
|
results.extend(_model_status(project_path))
|
|
230
|
+
|
|
231
|
+
# 检查 RIPPERDOC_* 环境变量
|
|
232
|
+
results.extend(_ripperdoc_env_status())
|
|
233
|
+
|
|
205
234
|
project_row = _project_status(project_path)
|
|
206
235
|
results.append(project_row)
|
|
207
236
|
|
|
@@ -18,6 +18,7 @@ from ripperdoc.utils.memory import (
|
|
|
18
18
|
MEMORY_FILE_NAME,
|
|
19
19
|
collect_all_memory_files,
|
|
20
20
|
)
|
|
21
|
+
from ripperdoc.utils.platform import is_windows
|
|
21
22
|
|
|
22
23
|
from .base import SlashCommand
|
|
23
24
|
|
|
@@ -89,7 +90,7 @@ def _determine_editor_command() -> Optional[List[str]]:
|
|
|
89
90
|
return shlex.split(value)
|
|
90
91
|
|
|
91
92
|
candidates = ["code", "nano", "vim", "vi"]
|
|
92
|
-
if
|
|
93
|
+
if is_windows():
|
|
93
94
|
candidates.insert(0, "notepad")
|
|
94
95
|
|
|
95
96
|
for candidate in candidates:
|
|
@@ -155,12 +155,34 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
155
155
|
context_prompt += "): "
|
|
156
156
|
context_window = parse_int(context_prompt, context_window_default)
|
|
157
157
|
|
|
158
|
+
# Vision support prompt
|
|
159
|
+
supports_vision_default = existing_profile.supports_vision if existing_profile else None
|
|
160
|
+
supports_vision = None
|
|
161
|
+
vision_default_display = (
|
|
162
|
+
"auto"
|
|
163
|
+
if supports_vision_default is None
|
|
164
|
+
else ("yes" if supports_vision_default else "no")
|
|
165
|
+
)
|
|
166
|
+
supports_vision_input = (
|
|
167
|
+
console.input(f"Supports vision (images)? [{vision_default_display}] (Y/n/auto): ")
|
|
168
|
+
.strip()
|
|
169
|
+
.lower()
|
|
170
|
+
)
|
|
171
|
+
if supports_vision_input in ("y", "yes"):
|
|
172
|
+
supports_vision = True
|
|
173
|
+
elif supports_vision_input in ("n", "no"):
|
|
174
|
+
supports_vision = False
|
|
175
|
+
elif supports_vision_input in ("auto", ""):
|
|
176
|
+
supports_vision = None
|
|
177
|
+
else:
|
|
178
|
+
supports_vision = supports_vision_default
|
|
179
|
+
|
|
158
180
|
default_set_main = (
|
|
159
181
|
not config.model_profiles
|
|
160
182
|
or getattr(config.model_pointers, "main", "") not in config.model_profiles
|
|
161
183
|
)
|
|
162
184
|
set_main_input = (
|
|
163
|
-
console.input(f"Set as main model?
|
|
185
|
+
console.input(f"Set as main model? ({'Y' if default_set_main else 'y'}/N): ")
|
|
164
186
|
.strip()
|
|
165
187
|
.lower()
|
|
166
188
|
)
|
|
@@ -175,6 +197,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
175
197
|
temperature=temperature,
|
|
176
198
|
context_window=context_window,
|
|
177
199
|
auth_token=auth_token,
|
|
200
|
+
supports_vision=supports_vision,
|
|
178
201
|
)
|
|
179
202
|
|
|
180
203
|
try:
|
|
@@ -276,6 +299,31 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
276
299
|
existing_profile.context_window,
|
|
277
300
|
)
|
|
278
301
|
|
|
302
|
+
# Vision support prompt
|
|
303
|
+
vision_default_display = (
|
|
304
|
+
"auto"
|
|
305
|
+
if existing_profile.supports_vision is None
|
|
306
|
+
else ("yes" if existing_profile.supports_vision else "no")
|
|
307
|
+
)
|
|
308
|
+
supports_vision_input = (
|
|
309
|
+
console.input(
|
|
310
|
+
f"Supports vision (images)? [{vision_default_display}] (Y/n/auto/C=clear): "
|
|
311
|
+
)
|
|
312
|
+
.strip()
|
|
313
|
+
.lower()
|
|
314
|
+
)
|
|
315
|
+
supports_vision = None
|
|
316
|
+
if supports_vision_input in ("y", "yes"):
|
|
317
|
+
supports_vision = True
|
|
318
|
+
elif supports_vision_input in ("n", "no"):
|
|
319
|
+
supports_vision = False
|
|
320
|
+
elif supports_vision_input in ("c", "clear", "-"):
|
|
321
|
+
supports_vision = None
|
|
322
|
+
elif supports_vision_input in ("auto", ""):
|
|
323
|
+
supports_vision = existing_profile.supports_vision
|
|
324
|
+
else:
|
|
325
|
+
supports_vision = existing_profile.supports_vision
|
|
326
|
+
|
|
279
327
|
updated_profile = ModelProfile(
|
|
280
328
|
provider=provider,
|
|
281
329
|
model=model_name,
|
|
@@ -285,6 +333,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
285
333
|
temperature=temperature,
|
|
286
334
|
context_window=context_window,
|
|
287
335
|
auth_token=auth_token,
|
|
336
|
+
supports_vision=supports_vision,
|
|
288
337
|
)
|
|
289
338
|
|
|
290
339
|
try:
|
|
@@ -353,10 +402,7 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
353
402
|
pointer = "main"
|
|
354
403
|
target = tokens[1]
|
|
355
404
|
else:
|
|
356
|
-
pointer = (
|
|
357
|
-
console.input("Pointer (main/quick) [main]: ").strip().lower()
|
|
358
|
-
or "main"
|
|
359
|
-
)
|
|
405
|
+
pointer = console.input("Pointer (main/quick) [main]: ").strip().lower() or "main"
|
|
360
406
|
if pointer not in valid_pointers:
|
|
361
407
|
console.print(
|
|
362
408
|
f"[red]Invalid pointer '{escape(pointer)}'. Valid pointers: {', '.join(valid_pointers)}[/red]"
|
|
@@ -415,6 +461,16 @@ def _handle(ui: Any, trimmed_arg: str) -> bool:
|
|
|
415
461
|
)
|
|
416
462
|
if profile.openai_tool_mode:
|
|
417
463
|
console.print(f" openai_tool_mode: {profile.openai_tool_mode}", markup=False)
|
|
464
|
+
if profile.thinking_mode:
|
|
465
|
+
console.print(f" thinking_mode: {profile.thinking_mode}", markup=False)
|
|
466
|
+
# Display vision support
|
|
467
|
+
if profile.supports_vision is None:
|
|
468
|
+
vision_display = "auto-detect"
|
|
469
|
+
elif profile.supports_vision:
|
|
470
|
+
vision_display = "yes"
|
|
471
|
+
else:
|
|
472
|
+
vision_display = "no"
|
|
473
|
+
console.print(f" supports_vision: {vision_display}", markup=False)
|
|
418
474
|
pointer_labels = ", ".join(f"{p}->{v or '-'}" for p, v in pointer_map.items())
|
|
419
475
|
console.print(f"[dim]Pointers: {escape(pointer_labels)}[/dim]")
|
|
420
476
|
return True
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from rich.markup import escape
|
|
2
|
+
|
|
3
|
+
from ripperdoc.core.skills import (
|
|
4
|
+
SkillDefinition,
|
|
5
|
+
SkillLoadResult,
|
|
6
|
+
SkillLocation,
|
|
7
|
+
load_all_skills,
|
|
8
|
+
skill_directories,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
from .base import SlashCommand
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _handle(ui: Any, _: str) -> bool:
|
|
16
|
+
console = ui.console
|
|
17
|
+
project_path = getattr(ui, "project_path", None)
|
|
18
|
+
|
|
19
|
+
result: SkillLoadResult = load_all_skills(project_path=project_path)
|
|
20
|
+
|
|
21
|
+
if not result.skills:
|
|
22
|
+
dirs = skill_directories(project_path=project_path)
|
|
23
|
+
dir_paths = [f"'{d}'" for d, _ in dirs]
|
|
24
|
+
console.print("[yellow]No skills found.[/yellow]")
|
|
25
|
+
console.print(
|
|
26
|
+
f"\n[bold]Create skills in:[/bold]\n"
|
|
27
|
+
f" • User: {dir_paths[0]}\n"
|
|
28
|
+
f" • Project: {dir_paths[1]}\n"
|
|
29
|
+
f"\n[dim]Each skill needs a SKILL.md file with YAML frontmatter:\n"
|
|
30
|
+
f"---\n"
|
|
31
|
+
f"name: my-skill\n"
|
|
32
|
+
f"description: A helpful skill\n"
|
|
33
|
+
f"---\n\n"
|
|
34
|
+
f"Skill content goes here...[/dim]"
|
|
35
|
+
)
|
|
36
|
+
if result.errors:
|
|
37
|
+
console.print("\n[bold red]Errors encountered while loading skills:[/bold red]")
|
|
38
|
+
for error in result.errors:
|
|
39
|
+
console.print(f" • {error.path}: {escape(error.reason)}", markup=False)
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
console.print("\n[bold]Skills[/bold]")
|
|
43
|
+
|
|
44
|
+
# Group skills by location for better organization
|
|
45
|
+
user_skills = [s for s in result.skills if s.location == SkillLocation.USER]
|
|
46
|
+
project_skills = [s for s in result.skills if s.location == SkillLocation.PROJECT]
|
|
47
|
+
other_skills = [s for s in result.skills if s.location == SkillLocation.OTHER]
|
|
48
|
+
|
|
49
|
+
def print_skill(skill: SkillDefinition) -> None:
|
|
50
|
+
location_tag = f"[dim]({skill.location.value})[/dim]" if skill.location else ""
|
|
51
|
+
console.print(f"\n[bold cyan]{escape(skill.name)}[/bold cyan] {location_tag}")
|
|
52
|
+
console.print(f" {escape(skill.description)}")
|
|
53
|
+
|
|
54
|
+
if skill.allowed_tools:
|
|
55
|
+
tools_str = ", ".join(escape(t) for t in skill.allowed_tools)
|
|
56
|
+
console.print(f" Tools: {tools_str}")
|
|
57
|
+
|
|
58
|
+
if skill.model:
|
|
59
|
+
console.print(f" Model: {escape(skill.model)}")
|
|
60
|
+
|
|
61
|
+
if skill.max_thinking_tokens:
|
|
62
|
+
console.print(f" Max thinking tokens: {skill.max_thinking_tokens}")
|
|
63
|
+
|
|
64
|
+
if skill.skill_type != "prompt":
|
|
65
|
+
console.print(f" Type: {escape(skill.skill_type)}")
|
|
66
|
+
|
|
67
|
+
if skill.disable_model_invocation:
|
|
68
|
+
console.print(" [yellow]Model invocation disabled[/yellow]")
|
|
69
|
+
|
|
70
|
+
console.print(f" Path: {escape(str(skill.path))}", markup=False)
|
|
71
|
+
|
|
72
|
+
# Print project skills first (they have priority), then user skills
|
|
73
|
+
if project_skills:
|
|
74
|
+
console.print("\n[bold]Project skills:[/bold]")
|
|
75
|
+
for skill in project_skills:
|
|
76
|
+
print_skill(skill)
|
|
77
|
+
|
|
78
|
+
if user_skills:
|
|
79
|
+
console.print("\n[bold]User skills:[/bold]")
|
|
80
|
+
for skill in user_skills:
|
|
81
|
+
print_skill(skill)
|
|
82
|
+
|
|
83
|
+
if other_skills:
|
|
84
|
+
console.print("\n[bold]Other skills:[/bold]")
|
|
85
|
+
for skill in other_skills:
|
|
86
|
+
print_skill(skill)
|
|
87
|
+
|
|
88
|
+
if result.errors:
|
|
89
|
+
console.print("\n[bold red]Errors encountered while loading skills:[/bold red]")
|
|
90
|
+
for error in result.errors:
|
|
91
|
+
console.print(f" • {error.path}: {escape(error.reason)}", markup=False)
|
|
92
|
+
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
command = SlashCommand(
|
|
97
|
+
name="skills",
|
|
98
|
+
description="List available skills",
|
|
99
|
+
handler=_handle,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = ["command"]
|
|
@@ -165,7 +165,9 @@ def _handle(ui: Any, _args: str) -> bool:
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
# Row 2: Total tokens | Longest session
|
|
168
|
-
total_tokens_display =
|
|
168
|
+
total_tokens_display = (
|
|
169
|
+
format_large_number(stats.total_tokens) if stats.total_tokens > 0 else "0"
|
|
170
|
+
)
|
|
169
171
|
longest_session_display = ""
|
|
170
172
|
if stats.longest_session_duration.total_seconds() > 0:
|
|
171
173
|
longest_session_display = format_duration(stats.longest_session_duration)
|
|
@@ -212,9 +214,7 @@ def _handle(ui: Any, _args: str) -> bool:
|
|
|
212
214
|
# Create the main panel with statistics
|
|
213
215
|
# Match heatmap width: WEEKDAY_LABEL_WIDTH (8) + weeks_count (52) = 60
|
|
214
216
|
heatmap_width = 8 + weeks_count + 16
|
|
215
|
-
console.print(
|
|
216
|
-
Panel(table, title="Statistics", border_style="blue", width=heatmap_width)
|
|
217
|
-
)
|
|
217
|
+
console.print(Panel(table, title="Statistics", border_style="blue", width=heatmap_width))
|
|
218
218
|
console.print()
|
|
219
219
|
|
|
220
220
|
# Fun comparisons
|