rubber-ducky 1.5.2__py3-none-any.whl → 1.6.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.
ducky/ducky.py CHANGED
@@ -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
- await self._use_crumb(first_word)
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]/help[/bold]", "Show this help message"),
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
- ("[bold]/expand[/bold]", "Show full output of last shell command"),
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
- "[bold]/run[/bold]",
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
- console.print(f"{command:<45} {description}")
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 "/crumbs" - show list
711
- await self._show_crumbs()
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
- "Usage: /crumb <name> | /crumb add <name> <cmd> | /crumb del <name>",
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(f"Command: ", style="cyan", end="")
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
- result = await rubber_ducky.send_prompt(prompt=prompt, code=code)
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:
@@ -1029,9 +1124,15 @@ async def ducky() -> None:
1029
1124
  action="store_true",
1030
1125
  help="Run DuckY offline using a local Ollama instance on localhost:11434",
1031
1126
  )
1127
+ parser.add_argument(
1128
+ "--yolo",
1129
+ "-y",
1130
+ action="store_true",
1131
+ help=" Automatically run the suggested command without confirmation",
1132
+ )
1032
1133
  parser.add_argument(
1033
1134
  "single_prompt",
1034
- nargs="?",
1135
+ nargs="*",
1035
1136
  help="Run a single prompt and copy the suggested command to clipboard",
1036
1137
  default=None,
1037
1138
  )
@@ -1090,40 +1191,85 @@ async def ducky() -> None:
1090
1191
 
1091
1192
  # Handle crumb invocation mode
1092
1193
  crumb_manager = CrumbManager()
1093
- if args.single_prompt and crumb_manager.has_crumb(args.single_prompt):
1094
- crumb = crumb_manager.get_crumb(args.single_prompt)
1095
- if crumb:
1096
- explanation = crumb.get("explanation", "") or "No explanation"
1097
- command = crumb.get("command", "") or "No command"
1098
-
1099
- console.print(f"\n[bold cyan]Crumb: {args.single_prompt}[/bold cyan]")
1100
- console.print(f"Explanation: {explanation}", style="green")
1101
- console.print(f"Command: ", style="cyan", end="")
1102
- console.print(command, highlight=False)
1103
-
1104
- if command and command != "No command":
1105
- # Execute the command
1106
- await run_shell_and_print(
1107
- rubber_ducky,
1108
- command,
1109
- logger=logger,
1110
- history=rubber_ducky.messages,
1111
- )
1112
- return
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
1113
1223
 
1114
1224
  # Handle single prompt mode
1115
1225
  if args.single_prompt:
1226
+ prompt = " ".join(args.single_prompt)
1116
1227
  result = await run_single_prompt(
1117
- rubber_ducky, args.single_prompt, code=code, logger=logger
1228
+ rubber_ducky, prompt, code=code, logger=logger
1118
1229
  )
1119
1230
  if result.command:
1120
- if copy_to_clipboard(result.command):
1231
+ if args.yolo:
1232
+ await run_shell_and_print(
1233
+ rubber_ducky,
1234
+ result.command,
1235
+ logger=logger,
1236
+ history=rubber_ducky.messages,
1237
+ )
1238
+ elif copy_to_clipboard(result.command):
1121
1239
  console.print("\n[green]✓[/green] Command copied to clipboard")
1122
1240
  return
1123
1241
 
1124
1242
  await interactive_session(rubber_ducky, logger=logger, code=code)
1125
1243
 
1126
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
+
1127
1273
  def main() -> None:
1128
1274
  asyncio.run(ducky())
1129
1275
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rubber-ducky
3
- Version: 1.5.2
3
+ Version: 1.6.0
4
4
  Summary: Quick CLI do-it-all tool. Use natural language to spit out bash commands
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,13 +1,13 @@
1
1
  ducky/__init__.py,sha256=2vLhJxOuJ3lnIeg5rmF6xUvybUT5Qhjej6AS0BeBASY,60
2
2
  ducky/config.py,sha256=Lh7xTUYh4i8Gxgrl0oTYadZB_72Wy2BKIqLCcDQduOA,2116
3
3
  ducky/crumb.py,sha256=7BlyjD81-cZptYxQM97y6gOGdVDBF2qzxW0xbPqbspE,2693
4
- ducky/ducky.py,sha256=5B_if1sI06lXvVbDOyPt4nust6DeIOa28v1MC6pjtzg,41167
4
+ ducky/ducky.py,sha256=mPxbfk9LVPpYUz9nh6zsgCH7K2S-HV46FniIVMJbHm8,47342
5
5
  examples/POLLING_USER_GUIDE.md,sha256=rMEAczZhpgyJ9BgwHkN-SKwSdyas8nlw_CjpV7SFOLA,10685
6
6
  examples/mock-logs/info.txt,sha256=apJqEO__UM1R2_2x9MlQOA7XmxvLvbhRvOy-FAwrINo,258
7
7
  examples/mock-logs/mock-logs.sh,sha256=zM2JSaCR1eCQLlMvXDWjFnpxZTqrMpnFRa_SgNLPmBk,1132
8
- rubber_ducky-1.5.2.dist-info/licenses/LICENSE,sha256=gQ1rCmw18NqTk5GxG96F6vgyN70e1c4kcKUtWDwdNaE,1069
9
- rubber_ducky-1.5.2.dist-info/METADATA,sha256=AsaHxQ3vt2rl4dxYaZG4kIGsEwv8CB87oFFRit-WlTY,6733
10
- rubber_ducky-1.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- rubber_ducky-1.5.2.dist-info/entry_points.txt,sha256=WPnVUUNvWdMDcBlCo8JCzkLghGllMX5QVZyQghyq85Q,75
12
- rubber_ducky-1.5.2.dist-info/top_level.txt,sha256=hid_mDkugR6XIeravFKuzcRPpuN_ylN3ejC_06Fmnb4,15
13
- rubber_ducky-1.5.2.dist-info/RECORD,,
8
+ rubber_ducky-1.6.0.dist-info/licenses/LICENSE,sha256=gQ1rCmw18NqTk5GxG96F6vgyN70e1c4kcKUtWDwdNaE,1069
9
+ rubber_ducky-1.6.0.dist-info/METADATA,sha256=_Q7V5YvGVeDVBPPkeeKbw5hiIFM2u1R0TnQm5_9JTtU,6733
10
+ rubber_ducky-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ rubber_ducky-1.6.0.dist-info/entry_points.txt,sha256=WPnVUUNvWdMDcBlCo8JCzkLghGllMX5QVZyQghyq85Q,75
12
+ rubber_ducky-1.6.0.dist-info/top_level.txt,sha256=hid_mDkugR6XIeravFKuzcRPpuN_ylN3ejC_06Fmnb4,15
13
+ rubber_ducky-1.6.0.dist-info/RECORD,,