rubber-ducky 1.5.3__tar.gz → 1.6.0__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.5.3/rubber_ducky.egg-info → rubber_ducky-1.6.0}/PKG-INFO +1 -1
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/ducky/ducky.py +170 -37
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/pyproject.toml +1 -1
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0/rubber_ducky.egg-info}/PKG-INFO +1 -1
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/LICENSE +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/MANIFEST.in +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/README.md +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/ducky/__init__.py +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/ducky/config.py +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/ducky/crumb.py +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/examples/POLLING_USER_GUIDE.md +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/examples/mock-logs/info.txt +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/examples/mock-logs/mock-logs.sh +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/rubber_ducky.egg-info/SOURCES.txt +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/rubber_ducky.egg-info/dependency_links.txt +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/rubber_ducky.egg-info/entry_points.txt +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/rubber_ducky.egg-info/requires.txt +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/rubber_ducky.egg-info/top_level.txt +0 -0
- {rubber_ducky-1.5.3 → rubber_ducky-1.6.0}/setup.cfg +0 -0
|
@@ -348,9 +348,12 @@ class RubberDuck:
|
|
|
348
348
|
if host:
|
|
349
349
|
os.environ["OLLAMA_HOST"] = host
|
|
350
350
|
self.client = AsyncClient(host)
|
|
351
|
+
if "ollama.com" in host:
|
|
352
|
+
console.print("[dim]Note: Cloud models require authentication[/dim]", style="yellow")
|
|
351
353
|
elif "-cloud" in model_name:
|
|
352
354
|
os.environ["OLLAMA_HOST"] = "https://ollama.com"
|
|
353
355
|
self.client = AsyncClient("https://ollama.com")
|
|
356
|
+
console.print("[dim]Note: Cloud models require authentication[/dim]", style="yellow")
|
|
354
357
|
else:
|
|
355
358
|
os.environ["OLLAMA_HOST"] = "http://localhost:11434"
|
|
356
359
|
self.client = AsyncClient()
|
|
@@ -549,7 +552,10 @@ class InlineInterface:
|
|
|
549
552
|
# Check if first word is a crumb name
|
|
550
553
|
first_word = stripped.split()[0].lower()
|
|
551
554
|
if self.crumb_manager.has_crumb(first_word):
|
|
552
|
-
|
|
555
|
+
# Extract additional arguments after the crumb name
|
|
556
|
+
parts = stripped.split()
|
|
557
|
+
args = parts[1:] if len(parts) > 1 else None
|
|
558
|
+
await self._use_crumb(first_word, args)
|
|
553
559
|
return
|
|
554
560
|
|
|
555
561
|
if stripped.lower() in {":run", "/run"}:
|
|
@@ -634,13 +640,16 @@ class InlineInterface:
|
|
|
634
640
|
console.print()
|
|
635
641
|
|
|
636
642
|
commands = [
|
|
637
|
-
("[bold]
|
|
643
|
+
("[bold]Crumbs:[/bold]", ""),
|
|
644
|
+
("[bold]/crumb help[/bold]", "Show detailed crumb commands help"),
|
|
638
645
|
("[bold]/crumbs[/bold]", "List all saved crumb shortcuts"),
|
|
639
646
|
("[bold]/crumb <name>[/bold]", "Save last result as a crumb"),
|
|
640
647
|
("[bold]/crumb add <name> <cmd>[/bold]", "Manually add a crumb"),
|
|
641
648
|
("[bold]/crumb del <name>[/bold]", "Delete a crumb"),
|
|
642
649
|
("[bold]<name>[/bold]", "Invoke a saved crumb"),
|
|
643
|
-
("
|
|
650
|
+
("", ""),
|
|
651
|
+
("[bold]General:[/bold]", ""),
|
|
652
|
+
("[bold]/help[/bold]", "Show this help message"),
|
|
644
653
|
("[bold]/model[/bold]", "Select a model interactively (local or cloud)"),
|
|
645
654
|
(
|
|
646
655
|
"[bold]/local[/bold]",
|
|
@@ -651,10 +660,8 @@ class InlineInterface:
|
|
|
651
660
|
"[bold]/clear[/bold] or [bold]/reset[/bold]",
|
|
652
661
|
"Clear conversation history",
|
|
653
662
|
),
|
|
654
|
-
(
|
|
655
|
-
|
|
656
|
-
"Re-run the last suggested command",
|
|
657
|
-
),
|
|
663
|
+
("[bold]/expand[/bold]", "Show full output of last shell command"),
|
|
664
|
+
("[bold]/run[/bold]", "Re-run the last suggested command"),
|
|
658
665
|
(
|
|
659
666
|
"[bold]Empty Enter[/bold]",
|
|
660
667
|
"Re-run suggested command or explain last output",
|
|
@@ -666,7 +673,13 @@ class InlineInterface:
|
|
|
666
673
|
]
|
|
667
674
|
|
|
668
675
|
for command, description in commands:
|
|
669
|
-
|
|
676
|
+
if command:
|
|
677
|
+
console.print(f"{command:<45} {description}")
|
|
678
|
+
else:
|
|
679
|
+
console.print()
|
|
680
|
+
|
|
681
|
+
console.print()
|
|
682
|
+
console.print("[dim]Use /crumb help for detailed crumb command documentation[/dim]")
|
|
670
683
|
|
|
671
684
|
console.print()
|
|
672
685
|
|
|
@@ -706,9 +719,15 @@ class InlineInterface:
|
|
|
706
719
|
async def _handle_crumb_command(self, command: str) -> None:
|
|
707
720
|
"""Handle /crumb commands."""
|
|
708
721
|
parts = command.split()
|
|
722
|
+
|
|
709
723
|
if len(parts) == 1:
|
|
710
|
-
# Just "/
|
|
711
|
-
await self.
|
|
724
|
+
# Just "/crumb" - show help
|
|
725
|
+
await self._show_crumb_help()
|
|
726
|
+
return
|
|
727
|
+
|
|
728
|
+
# Check for help flag or argument
|
|
729
|
+
if parts[1] in {"help", "--help", "-h"}:
|
|
730
|
+
await self._show_crumb_help()
|
|
712
731
|
return
|
|
713
732
|
|
|
714
733
|
if len(parts) == 2:
|
|
@@ -721,6 +740,7 @@ class InlineInterface:
|
|
|
721
740
|
# "/crumb add <name> <...command>"
|
|
722
741
|
if len(parts) < 4:
|
|
723
742
|
console.print("Usage: /crumb add <name> <command>", style="yellow")
|
|
743
|
+
console.print("Example: /crumb add deploy docker build -t app:latest", style="dim")
|
|
724
744
|
return
|
|
725
745
|
name = parts[2]
|
|
726
746
|
cmd = " ".join(parts[3:])
|
|
@@ -734,10 +754,50 @@ class InlineInterface:
|
|
|
734
754
|
return
|
|
735
755
|
|
|
736
756
|
console.print(
|
|
737
|
-
"
|
|
757
|
+
"Unknown crumb command. Use /crumb help for usage information.",
|
|
738
758
|
style="yellow",
|
|
739
759
|
)
|
|
740
760
|
|
|
761
|
+
async def _show_crumb_help(self) -> None:
|
|
762
|
+
"""Display detailed help for crumb commands."""
|
|
763
|
+
console.print("\n[bold blue]Crumbs Help[/bold blue]")
|
|
764
|
+
console.print("=" * 40)
|
|
765
|
+
console.print()
|
|
766
|
+
|
|
767
|
+
console.print("[bold cyan]Commands:[/bold cyan]")
|
|
768
|
+
console.print()
|
|
769
|
+
|
|
770
|
+
commands = [
|
|
771
|
+
("[bold]/crumbs[/bold]", "List all saved crumb shortcuts"),
|
|
772
|
+
("[bold]/crumb help[/bold]", "Show this help message"),
|
|
773
|
+
("[bold]/crumb <name>[/bold]", "Save the last AI-suggested command as a crumb"),
|
|
774
|
+
("[bold]/crumb add <name> <cmd>[/bold]", "Manually add a crumb with a specific command"),
|
|
775
|
+
("[bold]/crumb del <name>[/bold]", "Delete a saved crumb"),
|
|
776
|
+
("[bold]<name>[/bold]", "Invoke a saved crumb by name"),
|
|
777
|
+
]
|
|
778
|
+
|
|
779
|
+
for command, description in commands:
|
|
780
|
+
console.print(f"{command:<45} {description}")
|
|
781
|
+
|
|
782
|
+
console.print()
|
|
783
|
+
console.print("[bold cyan]Examples:[/bold cyan]")
|
|
784
|
+
console.print()
|
|
785
|
+
console.print(" [dim]# List all crumbs[/dim]")
|
|
786
|
+
console.print(" >> /crumbs")
|
|
787
|
+
console.print()
|
|
788
|
+
console.print(" [dim]# Save last command as 'deploy'[/dim]")
|
|
789
|
+
console.print(" >> /crumb deploy")
|
|
790
|
+
console.print()
|
|
791
|
+
console.print(" [dim]# Manually add a crumb[/dim]")
|
|
792
|
+
console.print(" >> /crumb add test-run pytest tests/ -v")
|
|
793
|
+
console.print()
|
|
794
|
+
console.print(" [dim]# Delete a crumb[/dim]")
|
|
795
|
+
console.print(" >> /crumb del deploy")
|
|
796
|
+
console.print()
|
|
797
|
+
console.print(" [dim]# Run a saved crumb[/dim]")
|
|
798
|
+
console.print(" >> test-run")
|
|
799
|
+
console.print()
|
|
800
|
+
|
|
741
801
|
async def _save_crumb(self, name: str) -> None:
|
|
742
802
|
"""Save the last result as a crumb."""
|
|
743
803
|
if not self.assistant.last_result:
|
|
@@ -819,8 +879,13 @@ class InlineInterface:
|
|
|
819
879
|
else:
|
|
820
880
|
console.print(f"Crumb '{name}' not found.", style="yellow")
|
|
821
881
|
|
|
822
|
-
async def _use_crumb(self, name: str) -> None:
|
|
823
|
-
"""Recall and execute a saved crumb.
|
|
882
|
+
async def _use_crumb(self, name: str, args: list[str] | None = None) -> None:
|
|
883
|
+
"""Recall and execute a saved crumb.
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
name: Name of the crumb to execute
|
|
887
|
+
args: Optional list of arguments to replace ${VAR} placeholders in the command
|
|
888
|
+
"""
|
|
824
889
|
crumb = self.crumb_manager.get_crumb(name)
|
|
825
890
|
if not crumb:
|
|
826
891
|
console.print(f"Crumb '{name}' not found.", style="yellow")
|
|
@@ -829,9 +894,13 @@ class InlineInterface:
|
|
|
829
894
|
explanation = crumb.get("explanation", "") or "No explanation"
|
|
830
895
|
command = crumb.get("command", "") or "No command"
|
|
831
896
|
|
|
897
|
+
# Substitute placeholders with provided arguments
|
|
898
|
+
if args and command != "No command":
|
|
899
|
+
command = substitute_placeholders(command, args)
|
|
900
|
+
|
|
832
901
|
console.print(f"\n[bold cyan]Crumb: {name}[/bold cyan]")
|
|
833
902
|
console.print(f"Explanation: {explanation}", style="green")
|
|
834
|
-
console.print(
|
|
903
|
+
console.print("Command: ", style="cyan", end="")
|
|
835
904
|
console.print(command, highlight=False)
|
|
836
905
|
|
|
837
906
|
if command and command != "No command":
|
|
@@ -975,7 +1044,33 @@ async def run_single_prompt(
|
|
|
975
1044
|
) -> AssistantResult:
|
|
976
1045
|
if logger:
|
|
977
1046
|
logger.log_user(prompt)
|
|
978
|
-
|
|
1047
|
+
try:
|
|
1048
|
+
result = await rubber_ducky.send_prompt(prompt=prompt, code=code)
|
|
1049
|
+
except Exception as e:
|
|
1050
|
+
error_msg = str(e)
|
|
1051
|
+
if "unauthorized" in error_msg.lower() or "401" in error_msg:
|
|
1052
|
+
console.print("\n[red]Authentication Error (401)[/red]")
|
|
1053
|
+
console.print("You're trying to use a cloud model but don't have valid credentials.", style="yellow")
|
|
1054
|
+
|
|
1055
|
+
# Check if API key is set
|
|
1056
|
+
api_key = os.environ.get("OLLAMA_API_KEY")
|
|
1057
|
+
if api_key:
|
|
1058
|
+
console.print("\nAn OLLAMA_API_KEY is set, but it appears invalid.", style="yellow")
|
|
1059
|
+
else:
|
|
1060
|
+
console.print("\n[bold]OLLAMA_API_KEY environment variable is not set.[/bold]", style="yellow")
|
|
1061
|
+
|
|
1062
|
+
console.print("\nOptions:", style="bold")
|
|
1063
|
+
console.print(" 1. Use --local flag to access local models:", style="dim")
|
|
1064
|
+
console.print(" ducky --local", style="cyan")
|
|
1065
|
+
console.print(" 2. Select a local model with /local command", style="dim")
|
|
1066
|
+
console.print(" 3. Set up Ollama cloud API credentials:", style="dim")
|
|
1067
|
+
console.print(" export OLLAMA_API_KEY='your-api-key-here'", style="cyan")
|
|
1068
|
+
console.print("\nGet your API key from: https://ollama.com/account/api-keys", style="dim")
|
|
1069
|
+
console.print()
|
|
1070
|
+
raise
|
|
1071
|
+
else:
|
|
1072
|
+
raise
|
|
1073
|
+
|
|
979
1074
|
content = result.content or "(No content returned.)"
|
|
980
1075
|
console.print(content, style="green", highlight=False)
|
|
981
1076
|
if logger:
|
|
@@ -1037,7 +1132,7 @@ async def ducky() -> None:
|
|
|
1037
1132
|
)
|
|
1038
1133
|
parser.add_argument(
|
|
1039
1134
|
"single_prompt",
|
|
1040
|
-
nargs="
|
|
1135
|
+
nargs="*",
|
|
1041
1136
|
help="Run a single prompt and copy the suggested command to clipboard",
|
|
1042
1137
|
default=None,
|
|
1043
1138
|
)
|
|
@@ -1096,31 +1191,41 @@ async def ducky() -> None:
|
|
|
1096
1191
|
|
|
1097
1192
|
# Handle crumb invocation mode
|
|
1098
1193
|
crumb_manager = CrumbManager()
|
|
1099
|
-
if args.single_prompt
|
|
1100
|
-
|
|
1101
|
-
if
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
)
|
|
1118
|
-
|
|
1194
|
+
if args.single_prompt:
|
|
1195
|
+
first_arg = args.single_prompt[0]
|
|
1196
|
+
if crumb_manager.has_crumb(first_arg):
|
|
1197
|
+
# Extract crumb arguments (everything after the crumb name)
|
|
1198
|
+
crumb_args = args.single_prompt[1:] if len(args.single_prompt) > 1 else None
|
|
1199
|
+
|
|
1200
|
+
crumb = crumb_manager.get_crumb(first_arg)
|
|
1201
|
+
if crumb:
|
|
1202
|
+
explanation = crumb.get("explanation", "") or "No explanation"
|
|
1203
|
+
command = crumb.get("command", "") or "No command"
|
|
1204
|
+
|
|
1205
|
+
# Substitute placeholders with provided arguments
|
|
1206
|
+
if crumb_args and command != "No command":
|
|
1207
|
+
command = substitute_placeholders(command, crumb_args)
|
|
1208
|
+
|
|
1209
|
+
console.print(f"\n[bold cyan]Crumb: {first_arg}[/bold cyan]")
|
|
1210
|
+
console.print(f"Explanation: {explanation}", style="green")
|
|
1211
|
+
console.print("Command: ", style="cyan", end="")
|
|
1212
|
+
console.print(command, highlight=False)
|
|
1213
|
+
|
|
1214
|
+
if command and command != "No command":
|
|
1215
|
+
# Execute the command
|
|
1216
|
+
await run_shell_and_print(
|
|
1217
|
+
rubber_ducky,
|
|
1218
|
+
command,
|
|
1219
|
+
logger=logger,
|
|
1220
|
+
history=rubber_ducky.messages,
|
|
1221
|
+
)
|
|
1222
|
+
return
|
|
1119
1223
|
|
|
1120
1224
|
# Handle single prompt mode
|
|
1121
1225
|
if args.single_prompt:
|
|
1226
|
+
prompt = " ".join(args.single_prompt)
|
|
1122
1227
|
result = await run_single_prompt(
|
|
1123
|
-
rubber_ducky,
|
|
1228
|
+
rubber_ducky, prompt, code=code, logger=logger
|
|
1124
1229
|
)
|
|
1125
1230
|
if result.command:
|
|
1126
1231
|
if args.yolo:
|
|
@@ -1137,6 +1242,34 @@ async def ducky() -> None:
|
|
|
1137
1242
|
await interactive_session(rubber_ducky, logger=logger, code=code)
|
|
1138
1243
|
|
|
1139
1244
|
|
|
1245
|
+
def substitute_placeholders(command: str, args: list[str]) -> str:
|
|
1246
|
+
"""Replace ${VAR} placeholders in command with provided arguments.
|
|
1247
|
+
|
|
1248
|
+
Args:
|
|
1249
|
+
command: The command string with placeholders
|
|
1250
|
+
args: List of arguments to substitute (first arg replaces first placeholder, etc.)
|
|
1251
|
+
|
|
1252
|
+
Returns:
|
|
1253
|
+
Command with placeholders replaced, falling back to env vars for unreplaced placeholders
|
|
1254
|
+
"""
|
|
1255
|
+
result = command
|
|
1256
|
+
arg_index = 0
|
|
1257
|
+
placeholder_pattern = re.compile(r'\$\{([^}]+)\}')
|
|
1258
|
+
|
|
1259
|
+
def replace_placeholder(match: re.Match) -> str:
|
|
1260
|
+
nonlocal arg_index
|
|
1261
|
+
if arg_index < len(args):
|
|
1262
|
+
value = args[arg_index]
|
|
1263
|
+
arg_index += 1
|
|
1264
|
+
return value
|
|
1265
|
+
# Fallback to environment variable
|
|
1266
|
+
var_name = match.group(1)
|
|
1267
|
+
return os.environ.get(var_name, match.group(0))
|
|
1268
|
+
|
|
1269
|
+
result = placeholder_pattern.sub(replace_placeholder, result)
|
|
1270
|
+
return result
|
|
1271
|
+
|
|
1272
|
+
|
|
1140
1273
|
def main() -> None:
|
|
1141
1274
|
asyncio.run(ducky())
|
|
1142
1275
|
|
|
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
|