rubber-ducky 1.6.2__tar.gz → 1.6.5__tar.gz
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.
- {rubber_ducky-1.6.2/rubber_ducky.egg-info → rubber_ducky-1.6.5}/PKG-INFO +1 -1
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/ducky/ducky.py +208 -69
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/pyproject.toml +1 -1
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5/rubber_ducky.egg-info}/PKG-INFO +1 -1
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/LICENSE +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/MANIFEST.in +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/README.md +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/ducky/__init__.py +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/ducky/config.py +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/ducky/crumb.py +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/examples/POLLING_USER_GUIDE.md +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/examples/mock-logs/info.txt +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/examples/mock-logs/mock-logs.sh +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/rubber_ducky.egg-info/SOURCES.txt +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/rubber_ducky.egg-info/dependency_links.txt +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/rubber_ducky.egg-info/entry_points.txt +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/rubber_ducky.egg-info/requires.txt +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/rubber_ducky.egg-info/top_level.txt +0 -0
- {rubber_ducky-1.6.2 → rubber_ducky-1.6.5}/setup.cfg +0 -0
|
@@ -5,7 +5,6 @@ import asyncio
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
-
import shlex
|
|
9
8
|
import subprocess
|
|
10
9
|
import sys
|
|
11
10
|
from dataclasses import dataclass
|
|
@@ -15,7 +14,7 @@ from pathlib import Path
|
|
|
15
14
|
from textwrap import dedent
|
|
16
15
|
from typing import Any, Dict, List
|
|
17
16
|
|
|
18
|
-
__version__ = "1.6.
|
|
17
|
+
__version__ = "1.6.4"
|
|
19
18
|
|
|
20
19
|
from .config import ConfigManager
|
|
21
20
|
from .crumb import CrumbManager
|
|
@@ -36,6 +35,7 @@ try: # prompt_toolkit is optional at runtime
|
|
|
36
35
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
37
36
|
from prompt_toolkit.styles import Style
|
|
38
37
|
from prompt_toolkit.widgets import Box, Button, Dialog, Label, TextArea
|
|
38
|
+
from prompt_toolkit.formatted_text import PygmentsTokens
|
|
39
39
|
except ImportError: # pragma: no cover - fallback mode
|
|
40
40
|
PromptSession = None # type: ignore[assignment]
|
|
41
41
|
FileHistory = None # type: ignore[assignment]
|
|
@@ -43,6 +43,9 @@ except ImportError: # pragma: no cover - fallback mode
|
|
|
43
43
|
|
|
44
44
|
def patch_stdout() -> nullcontext:
|
|
45
45
|
return nullcontext()
|
|
46
|
+
else:
|
|
47
|
+
def patch_stdout() -> nullcontext:
|
|
48
|
+
return nullcontext()
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
@dataclass
|
|
@@ -123,22 +126,22 @@ def print_shell_result(result: ShellResult, truncate: bool = True) -> None:
|
|
|
123
126
|
# Show first 8 lines of stdout
|
|
124
127
|
show_lines = stdout_lines[:8]
|
|
125
128
|
console.print('\n'.join(show_lines), highlight=False)
|
|
126
|
-
console.print(f"... ({len(stdout_lines) - 8} more lines, use /expand to see full output)", style="dim
|
|
129
|
+
console.print(f"... ({len(stdout_lines) - 8} more lines, use /expand to see full output)", style="dim")
|
|
127
130
|
else:
|
|
128
131
|
console.print(result.stdout.rstrip(), highlight=False)
|
|
129
132
|
|
|
130
133
|
if result.stderr.strip():
|
|
131
134
|
if result.stdout.strip():
|
|
132
135
|
console.print()
|
|
133
|
-
console.print("[stderr]", style="bold
|
|
136
|
+
console.print("[stderr]", style="bold yellow")
|
|
134
137
|
if should_truncate:
|
|
135
138
|
# Show first 5 lines of stderr
|
|
136
139
|
show_lines = stderr_lines[:5]
|
|
137
|
-
console.print('\n'.join(show_lines), style="
|
|
140
|
+
console.print('\n'.join(show_lines), style="yellow", highlight=False)
|
|
138
141
|
if len(stderr_lines) > 5:
|
|
139
|
-
console.print(f"... ({len(stderr_lines) - 5} more lines)", style="dim
|
|
142
|
+
console.print(f"... ({len(stderr_lines) - 5} more lines)", style="dim")
|
|
140
143
|
else:
|
|
141
|
-
console.print(result.stderr.rstrip(), style="
|
|
144
|
+
console.print(result.stderr.rstrip(), style="yellow", highlight=False)
|
|
142
145
|
|
|
143
146
|
if result.returncode != 0 or (not result.stdout.strip() and not result.stderr.strip()):
|
|
144
147
|
suffix = (
|
|
@@ -159,7 +162,7 @@ async def run_shell_and_print(
|
|
|
159
162
|
if not command:
|
|
160
163
|
console.print("No command provided.", style="yellow")
|
|
161
164
|
return ShellResult(command="", stdout="", stderr="", returncode=-1)
|
|
162
|
-
console.print(f"$ {command}", style="bold
|
|
165
|
+
console.print(f"$ {command}", style="bold white")
|
|
163
166
|
result = await assistant.run_shell_command(command)
|
|
164
167
|
print_shell_result(result)
|
|
165
168
|
if logger:
|
|
@@ -235,8 +238,9 @@ class RubberDuck:
|
|
|
235
238
|
|
|
236
239
|
if effective_command_mode:
|
|
237
240
|
instruction = (
|
|
238
|
-
"Return a single bash command that accomplishes the task
|
|
239
|
-
"
|
|
241
|
+
"Return a single bash command that accomplishes the task wrapped in <command></command> tags. "
|
|
242
|
+
"You can write explanatory text before or after the command tags for the user, "
|
|
243
|
+
"but the command itself must be within the <command></command> tags and nothing else."
|
|
240
244
|
)
|
|
241
245
|
user_content = (
|
|
242
246
|
f"{user_content}\n\n{instruction}" if user_content else instruction
|
|
@@ -285,10 +289,20 @@ class RubberDuck:
|
|
|
285
289
|
)
|
|
286
290
|
|
|
287
291
|
def _extract_command(self, content: str) -> str | None:
|
|
288
|
-
|
|
289
|
-
if not
|
|
292
|
+
content = content.strip()
|
|
293
|
+
if not content:
|
|
290
294
|
return None
|
|
291
295
|
|
|
296
|
+
# First, try to extract command from <command></command> tags
|
|
297
|
+
command_match = re.search(r"<command>(.*?)</command>", content, re.DOTALL)
|
|
298
|
+
if command_match:
|
|
299
|
+
command = command_match.group(1).strip()
|
|
300
|
+
# Strip backticks if present
|
|
301
|
+
command = self._strip_backticks(command)
|
|
302
|
+
return command or None
|
|
303
|
+
|
|
304
|
+
# Fallback to code block detection
|
|
305
|
+
lines = content.splitlines()
|
|
292
306
|
command_lines: List[str] = []
|
|
293
307
|
in_block = False
|
|
294
308
|
|
|
@@ -313,9 +327,20 @@ class RubberDuck:
|
|
|
313
327
|
|
|
314
328
|
# Join all command lines with newlines for multi-line commands
|
|
315
329
|
command = "\n".join(command_lines)
|
|
330
|
+
# Strip backticks if present
|
|
331
|
+
command = self._strip_backticks(command)
|
|
316
332
|
|
|
317
333
|
return command or None
|
|
318
334
|
|
|
335
|
+
def _strip_backticks(self, command: str) -> str:
|
|
336
|
+
"""Strip surrounding backticks from a command string."""
|
|
337
|
+
command = command.strip()
|
|
338
|
+
if command.startswith("`"):
|
|
339
|
+
command = command[1:]
|
|
340
|
+
if command.endswith("`"):
|
|
341
|
+
command = command[:-1]
|
|
342
|
+
return command.strip()
|
|
343
|
+
|
|
319
344
|
async def list_models(self, host: str = "") -> list[str]:
|
|
320
345
|
"""List available Ollama models."""
|
|
321
346
|
# Set the host temporarily for this operation
|
|
@@ -330,7 +355,7 @@ class RubberDuck:
|
|
|
330
355
|
models.append(m.model)
|
|
331
356
|
return models
|
|
332
357
|
except Exception as e:
|
|
333
|
-
console.print(f"Error listing models: {e}", style="
|
|
358
|
+
console.print(f"Error listing models: {e}", style="yellow")
|
|
334
359
|
return []
|
|
335
360
|
finally:
|
|
336
361
|
# Restore original host
|
|
@@ -359,13 +384,27 @@ class RubberDuck:
|
|
|
359
384
|
os.environ["OLLAMA_HOST"] = "http://localhost:11434"
|
|
360
385
|
self.client = AsyncClient()
|
|
361
386
|
|
|
362
|
-
console.print(f"Switched to model: {model_name}", style="
|
|
387
|
+
console.print(f"Switched to model: {model_name}", style="yellow")
|
|
363
388
|
|
|
364
389
|
def clear_history(self) -> None:
|
|
365
390
|
"""Reset conversation history to the initial system prompt."""
|
|
366
391
|
if self.messages:
|
|
367
392
|
self.messages = [self.messages[0]]
|
|
368
|
-
console.print("Conversation history cleared.", style="
|
|
393
|
+
console.print("Conversation history cleared.", style="yellow")
|
|
394
|
+
|
|
395
|
+
async def check_connection(self) -> tuple[bool, str]:
|
|
396
|
+
"""Check if Ollama host is reachable. Returns (is_connected, message)."""
|
|
397
|
+
try:
|
|
398
|
+
models = await self.list_models()
|
|
399
|
+
return True, f"Connected ({len(models)} models available)"
|
|
400
|
+
except Exception as e:
|
|
401
|
+
error_msg = str(e).lower()
|
|
402
|
+
if "refused" in error_msg:
|
|
403
|
+
return False, "Connection refused - is Ollama running?"
|
|
404
|
+
elif "timeout" in error_msg:
|
|
405
|
+
return False, "Connection timeout - check network/host"
|
|
406
|
+
else:
|
|
407
|
+
return False, f"Error: {e}"
|
|
369
408
|
|
|
370
409
|
|
|
371
410
|
class InlineInterface:
|
|
@@ -374,6 +413,7 @@ class InlineInterface:
|
|
|
374
413
|
assistant: RubberDuck,
|
|
375
414
|
logger: ConversationLogger | None = None,
|
|
376
415
|
code: str | None = None,
|
|
416
|
+
quiet_mode: bool = False,
|
|
377
417
|
) -> None:
|
|
378
418
|
ensure_history_dir()
|
|
379
419
|
self.assistant = assistant
|
|
@@ -387,6 +427,7 @@ class InlineInterface:
|
|
|
387
427
|
self.session: PromptSession | None = None
|
|
388
428
|
self.selected_model: str | None = None
|
|
389
429
|
self.crumb_manager = CrumbManager()
|
|
430
|
+
self.quiet_mode = quiet_mode
|
|
390
431
|
|
|
391
432
|
if (
|
|
392
433
|
PromptSession is not None
|
|
@@ -426,6 +467,36 @@ class InlineInterface:
|
|
|
426
467
|
|
|
427
468
|
return kb
|
|
428
469
|
|
|
470
|
+
def _print_banner(self) -> None:
|
|
471
|
+
"""Print the startup banner with version, model info, and crumb count."""
|
|
472
|
+
from .ducky import __version__
|
|
473
|
+
|
|
474
|
+
# Determine model display with host indicator
|
|
475
|
+
model = self.assistant.model
|
|
476
|
+
host = os.environ.get("OLLAMA_HOST", "")
|
|
477
|
+
if host == "https://ollama.com":
|
|
478
|
+
model_display = f"{model}:cloud"
|
|
479
|
+
elif "localhost" in host:
|
|
480
|
+
model_display = f"{model}:local"
|
|
481
|
+
else:
|
|
482
|
+
model_display = model
|
|
483
|
+
|
|
484
|
+
# Get crumb count
|
|
485
|
+
crumbs = self.crumb_manager.list_crumbs()
|
|
486
|
+
crumb_count = len(crumbs)
|
|
487
|
+
|
|
488
|
+
# Print banner with yellow/white color scheme
|
|
489
|
+
console.print(f"Ducky v{__version__}", style="yellow")
|
|
490
|
+
console.print(
|
|
491
|
+
f"Model: [bold white]{model_display}[/bold white] | Crumbs: {crumb_count}"
|
|
492
|
+
)
|
|
493
|
+
console.print()
|
|
494
|
+
console.print(
|
|
495
|
+
"Enter submits • !cmd=shell • Ctrl+D=exit • /help=commands",
|
|
496
|
+
style="dim",
|
|
497
|
+
)
|
|
498
|
+
console.print()
|
|
499
|
+
|
|
429
500
|
async def run(self) -> None:
|
|
430
501
|
if self.session is None:
|
|
431
502
|
console.print(
|
|
@@ -435,10 +506,10 @@ class InlineInterface:
|
|
|
435
506
|
await self._run_basic_loop()
|
|
436
507
|
return
|
|
437
508
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
509
|
+
# Print banner if not in quiet mode
|
|
510
|
+
if not self.quiet_mode:
|
|
511
|
+
self._print_banner()
|
|
512
|
+
|
|
442
513
|
while True:
|
|
443
514
|
try:
|
|
444
515
|
with patch_stdout():
|
|
@@ -497,9 +568,9 @@ class InlineInterface:
|
|
|
497
568
|
)
|
|
498
569
|
process.communicate(input=command_to_copy)
|
|
499
570
|
|
|
500
|
-
console.print(f"Copied to clipboard: {command_to_copy}", style="
|
|
571
|
+
console.print(f"Copied to clipboard: {command_to_copy}", style="yellow")
|
|
501
572
|
except Exception as e:
|
|
502
|
-
console.print(f"Failed to copy to clipboard: {e}", style="
|
|
573
|
+
console.print(f"Failed to copy to clipboard: {e}", style="yellow")
|
|
503
574
|
console.print("You can manually copy the last command:", style="dim")
|
|
504
575
|
console.print(f" {self.last_command}", style="bold")
|
|
505
576
|
|
|
@@ -533,7 +604,7 @@ class InlineInterface:
|
|
|
533
604
|
return
|
|
534
605
|
|
|
535
606
|
console.print()
|
|
536
|
-
console.print(f"[Full output for: {self.last_shell_result.command}]", style="bold
|
|
607
|
+
console.print(f"[Full output for: {self.last_shell_result.command}]", style="bold white")
|
|
537
608
|
console.print()
|
|
538
609
|
print_shell_result(self.last_shell_result, truncate=False)
|
|
539
610
|
console.print()
|
|
@@ -554,8 +625,8 @@ class InlineInterface:
|
|
|
554
625
|
first_word = stripped.split()[0].lower()
|
|
555
626
|
if self.crumb_manager.has_crumb(first_word):
|
|
556
627
|
# Extract additional arguments after the crumb name
|
|
557
|
-
parts = stripped.split()
|
|
558
|
-
args = parts[1:]
|
|
628
|
+
parts = stripped.split(maxsplit=1)
|
|
629
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
559
630
|
await self._use_crumb(first_word, args)
|
|
560
631
|
return
|
|
561
632
|
|
|
@@ -636,8 +707,8 @@ class InlineInterface:
|
|
|
636
707
|
|
|
637
708
|
async def _show_help(self) -> None:
|
|
638
709
|
"""Display help information for all available commands."""
|
|
639
|
-
console.print("\nDucky CLI Help", style="bold
|
|
640
|
-
console.print("===============", style="bold
|
|
710
|
+
console.print("\nDucky CLI Help", style="bold white")
|
|
711
|
+
console.print("===============", style="bold white")
|
|
641
712
|
console.print()
|
|
642
713
|
|
|
643
714
|
commands = [
|
|
@@ -692,8 +763,8 @@ class InlineInterface:
|
|
|
692
763
|
console.print("No crumbs saved yet. Use '/crumb <name>' to save a command.", style="yellow")
|
|
693
764
|
return
|
|
694
765
|
|
|
695
|
-
console.print("\nSaved Crumbs", style="bold
|
|
696
|
-
console.print("=============", style="bold
|
|
766
|
+
console.print("\nSaved Crumbs", style="bold white")
|
|
767
|
+
console.print("=============", style="bold white")
|
|
697
768
|
console.print()
|
|
698
769
|
|
|
699
770
|
# Calculate max name length for alignment
|
|
@@ -706,7 +777,7 @@ class InlineInterface:
|
|
|
706
777
|
|
|
707
778
|
# Format: name | explanation | command
|
|
708
779
|
console.print(
|
|
709
|
-
f"[bold]{name:<{max_name_len}}[/bold] | [
|
|
780
|
+
f"[bold yellow]{name:<{max_name_len}}[/bold yellow] | [white]{explanation}[/white] | [dim]{command}[/dim]"
|
|
710
781
|
)
|
|
711
782
|
|
|
712
783
|
console.print(f"\n[dim]Total: {len(crumbs)} crumbs[/dim]")
|
|
@@ -823,7 +894,7 @@ class InlineInterface:
|
|
|
823
894
|
command=self.assistant.last_result.command,
|
|
824
895
|
)
|
|
825
896
|
|
|
826
|
-
console.print(f"Saved crumb '{name}'!", style="
|
|
897
|
+
console.print(f"Saved crumb '{name}'!", style="yellow")
|
|
827
898
|
console.print("Generating explanation...", style="dim")
|
|
828
899
|
|
|
829
900
|
# Spawn subprocess to generate explanation asynchronously
|
|
@@ -852,7 +923,7 @@ class InlineInterface:
|
|
|
852
923
|
clean_explanation = re.sub(r'\x1b\[([0-9;]*[mGK])', '', explanation)
|
|
853
924
|
|
|
854
925
|
text = Text()
|
|
855
|
-
text.append("Explanation added: ", style="
|
|
926
|
+
text.append("Explanation added: ", style="white")
|
|
856
927
|
text.append(clean_explanation)
|
|
857
928
|
console.print(text)
|
|
858
929
|
except Exception as e:
|
|
@@ -867,7 +938,7 @@ class InlineInterface:
|
|
|
867
938
|
command=command,
|
|
868
939
|
)
|
|
869
940
|
|
|
870
|
-
console.print(f"Added crumb '{name}'!", style="
|
|
941
|
+
console.print(f"Added crumb '{name}'!", style="yellow")
|
|
871
942
|
console.print("Generating explanation...", style="dim")
|
|
872
943
|
|
|
873
944
|
# Spawn subprocess to generate explanation asynchronously
|
|
@@ -876,7 +947,7 @@ class InlineInterface:
|
|
|
876
947
|
async def _delete_crumb(self, name: str) -> None:
|
|
877
948
|
"""Delete a crumb."""
|
|
878
949
|
if self.crumb_manager.delete_crumb(name):
|
|
879
|
-
console.print(f"Deleted crumb '{name}'.", style="
|
|
950
|
+
console.print(f"Deleted crumb '{name}'.", style="yellow")
|
|
880
951
|
else:
|
|
881
952
|
console.print(f"Crumb '{name}' not found.", style="yellow")
|
|
882
953
|
|
|
@@ -899,9 +970,13 @@ class InlineInterface:
|
|
|
899
970
|
if args and command != "No command":
|
|
900
971
|
command = substitute_placeholders(command, args)
|
|
901
972
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
973
|
+
from rich.text import Text
|
|
974
|
+
crumb_text = Text()
|
|
975
|
+
crumb_text.append("Crumb: ", style="bold yellow")
|
|
976
|
+
crumb_text.append(name, style="bold yellow")
|
|
977
|
+
console.print(f"\n{crumb_text}")
|
|
978
|
+
console.print(f"Explanation: {explanation}", style="yellow")
|
|
979
|
+
console.print("Command: ", style="white", end="")
|
|
905
980
|
console.print(command, highlight=False)
|
|
906
981
|
|
|
907
982
|
if command and command != "No command":
|
|
@@ -918,7 +993,7 @@ class InlineInterface:
|
|
|
918
993
|
return
|
|
919
994
|
|
|
920
995
|
# Show current model
|
|
921
|
-
console.print(f"Current model: {self.assistant.model}", style="bold
|
|
996
|
+
console.print(f"Current model: {self.assistant.model}", style="bold yellow")
|
|
922
997
|
|
|
923
998
|
# If no host specified, give user a choice between local and cloud
|
|
924
999
|
if not host:
|
|
@@ -940,17 +1015,17 @@ class InlineInterface:
|
|
|
940
1015
|
elif choice == "2":
|
|
941
1016
|
host = "https://ollama.com"
|
|
942
1017
|
else:
|
|
943
|
-
console.print("Invalid choice. Please select 1 or 2.", style="
|
|
1018
|
+
console.print("Invalid choice. Please select 1 or 2.", style="yellow")
|
|
944
1019
|
return
|
|
945
1020
|
except (ValueError, EOFError):
|
|
946
|
-
console.print("Invalid input.", style="
|
|
1021
|
+
console.print("Invalid input.", style="yellow")
|
|
947
1022
|
return
|
|
948
1023
|
|
|
949
1024
|
models = await self.assistant.list_models(host)
|
|
950
1025
|
if not models:
|
|
951
1026
|
if host == "http://localhost:11434":
|
|
952
1027
|
console.print(
|
|
953
|
-
"No local models available. Is Ollama running?", style="
|
|
1028
|
+
"No local models available. Is Ollama running?", style="yellow"
|
|
954
1029
|
)
|
|
955
1030
|
console.print("Start Ollama with: ollama serve", style="yellow")
|
|
956
1031
|
else:
|
|
@@ -966,7 +1041,7 @@ class InlineInterface:
|
|
|
966
1041
|
|
|
967
1042
|
for i, model in enumerate(models, 1):
|
|
968
1043
|
if model == self.assistant.model:
|
|
969
|
-
console.print(f"{i}. {model} (current)", style="
|
|
1044
|
+
console.print(f"{i}. [bold yellow]{model}[/bold yellow] (current)", style="yellow")
|
|
970
1045
|
else:
|
|
971
1046
|
console.print(f"{i}. {model}")
|
|
972
1047
|
|
|
@@ -985,14 +1060,14 @@ class InlineInterface:
|
|
|
985
1060
|
if 0 <= index < len(models):
|
|
986
1061
|
selected_model = models[index]
|
|
987
1062
|
else:
|
|
988
|
-
console.print("Invalid model number.", style="
|
|
1063
|
+
console.print("Invalid model number.", style="yellow")
|
|
989
1064
|
return
|
|
990
1065
|
else:
|
|
991
1066
|
# Check if it's a model name
|
|
992
1067
|
if choice in models:
|
|
993
1068
|
selected_model = choice
|
|
994
1069
|
else:
|
|
995
|
-
console.print("Invalid model name.", style="
|
|
1070
|
+
console.print("Invalid model name.", style="yellow")
|
|
996
1071
|
return
|
|
997
1072
|
|
|
998
1073
|
self.assistant.switch_model(selected_model, host)
|
|
@@ -1004,7 +1079,7 @@ class InlineInterface:
|
|
|
1004
1079
|
host or os.environ.get("OLLAMA_HOST", "http://localhost:11434"),
|
|
1005
1080
|
)
|
|
1006
1081
|
except (ValueError, EOFError):
|
|
1007
|
-
console.print("Invalid input.", style="
|
|
1082
|
+
console.print("Invalid input.", style="yellow")
|
|
1008
1083
|
|
|
1009
1084
|
async def _run_basic_loop(self) -> None: # pragma: no cover - fallback path
|
|
1010
1085
|
while True:
|
|
@@ -1042,11 +1117,12 @@ async def run_single_prompt(
|
|
|
1042
1117
|
code: str | None = None,
|
|
1043
1118
|
logger: ConversationLogger | None = None,
|
|
1044
1119
|
suppress_suggestion: bool = False,
|
|
1120
|
+
command_mode: bool | None = None,
|
|
1045
1121
|
) -> AssistantResult:
|
|
1046
1122
|
if logger:
|
|
1047
1123
|
logger.log_user(prompt)
|
|
1048
1124
|
try:
|
|
1049
|
-
result = await rubber_ducky.send_prompt(prompt=prompt, code=code)
|
|
1125
|
+
result = await rubber_ducky.send_prompt(prompt=prompt, code=code, command_mode=command_mode)
|
|
1050
1126
|
except Exception as e:
|
|
1051
1127
|
error_msg = str(e)
|
|
1052
1128
|
if "unauthorized" in error_msg.lower() or "401" in error_msg:
|
|
@@ -1062,10 +1138,10 @@ async def run_single_prompt(
|
|
|
1062
1138
|
|
|
1063
1139
|
console.print("\nOptions:", style="bold")
|
|
1064
1140
|
console.print(" 1. Use --local flag to access local models:", style="dim")
|
|
1065
|
-
console.print(" ducky --local", style="
|
|
1141
|
+
console.print(" ducky --local", style="white")
|
|
1066
1142
|
console.print(" 2. Select a local model with /local command", style="dim")
|
|
1067
1143
|
console.print(" 3. Set up Ollama cloud API credentials:", style="dim")
|
|
1068
|
-
console.print(" export OLLAMA_API_KEY='your-api-key-here'", style="
|
|
1144
|
+
console.print(" export OLLAMA_API_KEY='your-api-key-here'", style="white")
|
|
1069
1145
|
console.print("\nGet your API key from: https://ollama.com/account/api-keys", style="dim")
|
|
1070
1146
|
console.print()
|
|
1071
1147
|
raise
|
|
@@ -1073,12 +1149,14 @@ async def run_single_prompt(
|
|
|
1073
1149
|
raise
|
|
1074
1150
|
|
|
1075
1151
|
content = result.content or "(No content returned.)"
|
|
1076
|
-
|
|
1152
|
+
# Strip <command>...</command> tags from display output
|
|
1153
|
+
display_content = re.sub(r"<command>.*?</command>", "", content, flags=re.DOTALL).strip()
|
|
1154
|
+
console.print(display_content, highlight=False)
|
|
1077
1155
|
if logger:
|
|
1078
1156
|
logger.log_assistant(content, result.command)
|
|
1079
1157
|
if result.command and not suppress_suggestion:
|
|
1080
|
-
console.print("\nSuggested command:", style="
|
|
1081
|
-
console.print(result.command, style="bold
|
|
1158
|
+
console.print("\nSuggested command:", style="yellow", highlight=False)
|
|
1159
|
+
console.print(result.command, style="bold yellow", highlight=False)
|
|
1082
1160
|
return result
|
|
1083
1161
|
|
|
1084
1162
|
|
|
@@ -1108,8 +1186,9 @@ async def interactive_session(
|
|
|
1108
1186
|
rubber_ducky: RubberDuck,
|
|
1109
1187
|
logger: ConversationLogger | None = None,
|
|
1110
1188
|
code: str | None = None,
|
|
1189
|
+
quiet_mode: bool = False,
|
|
1111
1190
|
) -> None:
|
|
1112
|
-
ui = InlineInterface(rubber_ducky, logger=logger, code=code)
|
|
1191
|
+
ui = InlineInterface(rubber_ducky, logger=logger, code=code, quiet_mode=quiet_mode)
|
|
1113
1192
|
await ui.run()
|
|
1114
1193
|
|
|
1115
1194
|
|
|
@@ -1134,6 +1213,12 @@ async def ducky() -> None:
|
|
|
1134
1213
|
action="store_true",
|
|
1135
1214
|
help=" Automatically run the suggested command without confirmation",
|
|
1136
1215
|
)
|
|
1216
|
+
parser.add_argument(
|
|
1217
|
+
"--quiet",
|
|
1218
|
+
"-q",
|
|
1219
|
+
action="store_true",
|
|
1220
|
+
help="Suppress startup messages and help text",
|
|
1221
|
+
)
|
|
1137
1222
|
parser.add_argument(
|
|
1138
1223
|
"single_prompt",
|
|
1139
1224
|
nargs="*",
|
|
@@ -1175,26 +1260,64 @@ async def ducky() -> None:
|
|
|
1175
1260
|
|
|
1176
1261
|
if piped_prompt is not None:
|
|
1177
1262
|
if piped_prompt:
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
await run_shell_and_print(
|
|
1187
|
-
rubber_ducky,
|
|
1188
|
-
result.command,
|
|
1189
|
-
logger=logger,
|
|
1190
|
-
history=rubber_ducky.messages,
|
|
1263
|
+
# Check if user also provided a command-line prompt
|
|
1264
|
+
if args.single_prompt:
|
|
1265
|
+
# Combine piped content with user prompt
|
|
1266
|
+
# User prompt is the instruction, piped content is context
|
|
1267
|
+
user_prompt = " ".join(args.single_prompt)
|
|
1268
|
+
combined_prompt = (
|
|
1269
|
+
f"Context from stdin:\n```\n{piped_prompt}\n```\n\n"
|
|
1270
|
+
f"User request: {user_prompt}"
|
|
1191
1271
|
)
|
|
1272
|
+
# Disable command_mode for this scenario - user wants an explanation, not a command
|
|
1273
|
+
result = await run_single_prompt(
|
|
1274
|
+
rubber_ducky, combined_prompt, code=code, logger=logger, command_mode=False
|
|
1275
|
+
)
|
|
1276
|
+
else:
|
|
1277
|
+
# Only piped input - proceed with command mode (default behavior)
|
|
1278
|
+
result = await run_single_prompt(
|
|
1279
|
+
rubber_ducky, piped_prompt, code=code, logger=logger
|
|
1280
|
+
)
|
|
1281
|
+
if (
|
|
1282
|
+
result.command
|
|
1283
|
+
and sys.stdout.isatty()
|
|
1284
|
+
and confirm("Run suggested command?")
|
|
1285
|
+
):
|
|
1286
|
+
await run_shell_and_print(
|
|
1287
|
+
rubber_ducky,
|
|
1288
|
+
result.command,
|
|
1289
|
+
logger=logger,
|
|
1290
|
+
history=rubber_ducky.messages,
|
|
1291
|
+
)
|
|
1192
1292
|
else:
|
|
1193
1293
|
console.print("No input received from stdin.", style="yellow")
|
|
1194
1294
|
return
|
|
1195
1295
|
|
|
1196
|
-
# Handle crumb
|
|
1296
|
+
# Handle crumb list command
|
|
1197
1297
|
crumb_manager = CrumbManager()
|
|
1298
|
+
if args.single_prompt and args.single_prompt[0] == "crumbs":
|
|
1299
|
+
crumbs = crumb_manager.list_crumbs()
|
|
1300
|
+
|
|
1301
|
+
if not crumbs:
|
|
1302
|
+
console.print("No crumbs saved yet.", style="yellow")
|
|
1303
|
+
else:
|
|
1304
|
+
console.print("Saved Crumbs", style="bold white")
|
|
1305
|
+
console.print("=============", style="bold white")
|
|
1306
|
+
console.print()
|
|
1307
|
+
|
|
1308
|
+
max_name_len = max(len(name) for name in crumbs.keys())
|
|
1309
|
+
|
|
1310
|
+
for name, data in sorted(crumbs.items()):
|
|
1311
|
+
explanation = data.get("explanation", "") or "No explanation yet"
|
|
1312
|
+
command = data.get("command", "") or "No command"
|
|
1313
|
+
|
|
1314
|
+
console.print(
|
|
1315
|
+
f"[bold yellow]{name:<{max_name_len}}[/bold yellow] | [white]{explanation}[/white] | [dim]{command}[/dim]"
|
|
1316
|
+
)
|
|
1317
|
+
console.print(f"\n[dim]Total: {len(crumbs)} crumbs[/dim]")
|
|
1318
|
+
return
|
|
1319
|
+
|
|
1320
|
+
# Handle crumb invocation mode
|
|
1198
1321
|
if args.single_prompt:
|
|
1199
1322
|
first_arg = args.single_prompt[0]
|
|
1200
1323
|
if crumb_manager.has_crumb(first_arg):
|
|
@@ -1210,9 +1333,13 @@ async def ducky() -> None:
|
|
|
1210
1333
|
if crumb_args and command != "No command":
|
|
1211
1334
|
command = substitute_placeholders(command, crumb_args)
|
|
1212
1335
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1336
|
+
from rich.text import Text
|
|
1337
|
+
crumb_text = Text()
|
|
1338
|
+
crumb_text.append("Crumb: ", style="bold yellow")
|
|
1339
|
+
crumb_text.append(first_arg, style="bold yellow")
|
|
1340
|
+
console.print(f"\n{crumb_text}")
|
|
1341
|
+
console.print(f"Explanation: {explanation}", style="yellow")
|
|
1342
|
+
console.print("Command: ", style="white", end="")
|
|
1216
1343
|
console.print(command, highlight=False)
|
|
1217
1344
|
|
|
1218
1345
|
if command and command != "No command":
|
|
@@ -1243,7 +1370,19 @@ async def ducky() -> None:
|
|
|
1243
1370
|
console.print("\n[green]✓[/green] Command copied to clipboard")
|
|
1244
1371
|
return
|
|
1245
1372
|
|
|
1246
|
-
|
|
1373
|
+
# Validate model is available if using local
|
|
1374
|
+
if not args.single_prompt and not piped_prompt and last_host == "http://localhost:11434":
|
|
1375
|
+
connected = True
|
|
1376
|
+
try:
|
|
1377
|
+
models = await rubber_ducky.list_models()
|
|
1378
|
+
if args.model not in models:
|
|
1379
|
+
console.print(f"Model '{args.model}' not found locally.", style="yellow")
|
|
1380
|
+
console.print(f"Available: {', '.join(models[:5])}...", style="dim")
|
|
1381
|
+
console.print("Use /model to select, or run 'ollama pull <model>'", style="yellow")
|
|
1382
|
+
except Exception:
|
|
1383
|
+
pass
|
|
1384
|
+
|
|
1385
|
+
await interactive_session(rubber_ducky, logger=logger, code=code, quiet_mode=args.quiet)
|
|
1247
1386
|
|
|
1248
1387
|
|
|
1249
1388
|
def substitute_placeholders(command: str, args: list[str]) -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|