rubber-ducky 1.2.0__py3-none-any.whl → 1.2.1__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
@@ -25,6 +25,8 @@ except ImportError: # pragma: no cover - fallback mode
25
25
 
26
26
  def patch_stdout() -> nullcontext:
27
27
  return nullcontext()
28
+
29
+
28
30
  from rich.console import Console
29
31
 
30
32
 
@@ -110,6 +112,7 @@ async def run_shell_and_print(
110
112
  assistant: RubberDuck,
111
113
  command: str,
112
114
  logger: ConversationLogger | None = None,
115
+ history: list[dict[str, str]] | None = None,
113
116
  ) -> None:
114
117
  if not command:
115
118
  console.print("No command provided.", style="yellow")
@@ -119,6 +122,18 @@ async def run_shell_and_print(
119
122
  print_shell_result(result)
120
123
  if logger:
121
124
  logger.log_shell(result)
125
+ if history is not None:
126
+ history.append({"role": "user", "content": f"!{command}"})
127
+ combined_output: list[str] = []
128
+ if result.stdout.strip():
129
+ combined_output.append(result.stdout.rstrip())
130
+ if result.stderr.strip():
131
+ combined_output.append(f"[stderr]\n{result.stderr.rstrip()}")
132
+ if result.returncode != 0:
133
+ combined_output.append(f"(exit status {result.returncode})")
134
+ if not combined_output:
135
+ combined_output.append("(command produced no output)")
136
+ history.append({"role": "assistant", "content": "\n\n".join(combined_output)})
122
137
 
123
138
 
124
139
  class RubberDuck:
@@ -255,6 +270,8 @@ class InlineInterface:
255
270
  self.last_command: str | None = None
256
271
  self.code = code
257
272
  self._code_sent = False
273
+ self.last_shell_output: str | None = None
274
+ self.pending_command: str | None = None
258
275
  self.session: PromptSession | None = None
259
276
 
260
277
  if (
@@ -300,7 +317,7 @@ class InlineInterface:
300
317
  return
301
318
 
302
319
  console.print(
303
- "Enter submits • Ctrl+J inserts newline Ctrl+R reruns last command • '!cmd' runs shell • Ctrl+D exits",
320
+ "Enter submits • empty Enter reruns the last suggested command (or explains the last shell output) • '!cmd' runs shell • Ctrl+D exits",
304
321
  style="dim",
305
322
  )
306
323
  while True:
@@ -326,11 +343,26 @@ class InlineInterface:
326
343
  if not self.last_command:
327
344
  console.print("No suggested command available yet.", style="yellow")
328
345
  return
329
- await run_shell_and_print(self.assistant, self.last_command, logger=self.logger)
346
+ await run_shell_and_print(
347
+ self.assistant,
348
+ self.last_command,
349
+ logger=self.logger,
350
+ history=self.assistant.messages,
351
+ )
352
+ self.last_shell_output = True
353
+ self.pending_command = None
354
+ self.last_command = None
330
355
 
331
356
  async def _process_text(self, text: str) -> None:
332
357
  stripped = text.strip()
333
358
  if not stripped:
359
+ if self.pending_command:
360
+ await self._run_last_command()
361
+ return
362
+ if self.last_shell_output:
363
+ await self._explain_last_command()
364
+ return
365
+ console.print("Nothing to run yet.", style="yellow")
334
366
  return
335
367
 
336
368
  if stripped.lower() in {":run", "/run"}:
@@ -339,8 +371,13 @@ class InlineInterface:
339
371
 
340
372
  if stripped.startswith("!"):
341
373
  await run_shell_and_print(
342
- self.assistant, stripped[1:].strip(), logger=self.logger
374
+ self.assistant,
375
+ stripped[1:].strip(),
376
+ logger=self.logger,
377
+ history=self.assistant.messages,
343
378
  )
379
+ self.last_shell_output = True
380
+ self.pending_command = None
344
381
  return
345
382
 
346
383
  result = await run_single_prompt(
@@ -352,6 +389,26 @@ class InlineInterface:
352
389
  if self.code and not self._code_sent:
353
390
  self._code_sent = True
354
391
  self.last_command = result.command
392
+ self.pending_command = result.command
393
+ self.last_shell_output = None
394
+
395
+ async def _explain_last_command(self) -> None:
396
+ if not self.assistant.messages or len(self.assistant.messages) < 2:
397
+ console.print("No shell output to explain yet.", style="yellow")
398
+ return
399
+ last_entry = self.assistant.messages[-1]
400
+ if last_entry["role"] != "assistant":
401
+ console.print("No shell output to explain yet.", style="yellow")
402
+ return
403
+ prompt = (
404
+ "The user ran a shell command above. Summarize the key findings from the output, "
405
+ "highlight problems if any, and suggest next steps. Do NOT suggest a shell command or code snippet.\n\n"
406
+ f"{last_entry['content']}"
407
+ )
408
+ await run_single_prompt(
409
+ self.assistant, prompt, logger=self.logger, suppress_suggestion=True
410
+ )
411
+ self.last_shell_output = None
355
412
 
356
413
  async def _run_basic_loop(self) -> None: # pragma: no cover - fallback path
357
414
  while True:
@@ -388,6 +445,7 @@ async def run_single_prompt(
388
445
  prompt: str,
389
446
  code: str | None = None,
390
447
  logger: ConversationLogger | None = None,
448
+ suppress_suggestion: bool = False,
391
449
  ) -> AssistantResult:
392
450
  if logger:
393
451
  logger.log_user(prompt)
@@ -396,7 +454,7 @@ async def run_single_prompt(
396
454
  console.print(content, style="green", highlight=False)
397
455
  if logger:
398
456
  logger.log_assistant(content, result.command)
399
- if result.command:
457
+ if result.command and not suppress_suggestion:
400
458
  console.print("\nSuggested command:", style="cyan", highlight=False)
401
459
  console.print(result.command, style="bold cyan", highlight=False)
402
460
  return result
@@ -455,7 +513,10 @@ async def ducky() -> None:
455
513
  and confirm("Run suggested command?")
456
514
  ):
457
515
  await run_shell_and_print(
458
- rubber_ducky, result.command, logger=logger
516
+ rubber_ducky,
517
+ result.command,
518
+ logger=logger,
519
+ history=rubber_ducky.messages,
459
520
  )
460
521
  else:
461
522
  console.print("No input received from stdin.", style="yellow")
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rubber-ducky
3
- Version: 1.2.0
4
- Summary: For developers who can never remember the right bash command
3
+ Version: 1.2.1
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
7
7
  License-File: LICENSE
@@ -43,7 +43,7 @@ Both `ducky` and `rubber-ducky` executables map to the same CLI, so `uvx rubber-
43
43
  ### Inline Session (default)
44
44
 
45
45
  Launching `ducky` with no arguments opens the inline interface:
46
- - **Enter** submits; **Ctrl+J** inserts a newline (helpful when crafting multi-line prompts).
46
+ - **Enter** submits; **Ctrl+J** inserts a newline (helpful when crafting multi-line prompts). Hitting **Enter on an empty prompt** reruns the latest suggested command; if none exists yet, it explains the most recent shell output.
47
47
  - **Ctrl+R** re-runs the last suggested command.
48
48
  - Prefix any line with **`!`** (e.g., `!ls -la`) to run a shell command immediately.
49
49
  - Arrow keys browse prompt history, backed by `~/.ducky/prompt_history`.
@@ -0,0 +1,8 @@
1
+ ducky/__init__.py,sha256=9l8SmwX0t1BmITkcrzW9fVMPvD2LfgKLZlSXWzPJFSE,25
2
+ ducky/ducky.py,sha256=FWGkAnyWB8k6GxsAu5WkIxJ5mlnT9ymIAJsJf8ryTts,17347
3
+ rubber_ducky-1.2.1.dist-info/licenses/LICENSE,sha256=gQ1rCmw18NqTk5GxG96F6vgyN70e1c4kcKUtWDwdNaE,1069
4
+ rubber_ducky-1.2.1.dist-info/METADATA,sha256=MDt4yR-GtzqF4bB-j8s4kXt3tNUDYJ6H_7Mr6mLUEu0,3063
5
+ rubber_ducky-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ rubber_ducky-1.2.1.dist-info/entry_points.txt,sha256=WPnVUUNvWdMDcBlCo8JCzkLghGllMX5QVZyQghyq85Q,75
7
+ rubber_ducky-1.2.1.dist-info/top_level.txt,sha256=4Q75MONDNPpQ3o17bTu7RFuKwFhTIRzlXP3_LDWQQ30,6
8
+ rubber_ducky-1.2.1.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- ducky/__init__.py,sha256=9l8SmwX0t1BmITkcrzW9fVMPvD2LfgKLZlSXWzPJFSE,25
2
- ducky/ducky.py,sha256=9xTfSsox4Fcvrz1C90rp0CN0vSc-bEZPB-I50PvCXMM,14792
3
- rubber_ducky-1.2.0.dist-info/licenses/LICENSE,sha256=gQ1rCmw18NqTk5GxG96F6vgyN70e1c4kcKUtWDwdNaE,1069
4
- rubber_ducky-1.2.0.dist-info/METADATA,sha256=pQ6UzHKoaaDBq6pP51yBmMRBiyYEUGWZeu5Vj2hhmvQ,2915
5
- rubber_ducky-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
- rubber_ducky-1.2.0.dist-info/entry_points.txt,sha256=WPnVUUNvWdMDcBlCo8JCzkLghGllMX5QVZyQghyq85Q,75
7
- rubber_ducky-1.2.0.dist-info/top_level.txt,sha256=4Q75MONDNPpQ3o17bTu7RFuKwFhTIRzlXP3_LDWQQ30,6
8
- rubber_ducky-1.2.0.dist-info/RECORD,,