rubber-ducky 1.6.4__py3-none-any.whl → 1.6.6__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
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  from textwrap import dedent
15
15
  from typing import Any, Dict, List
16
16
 
17
- __version__ = "1.6.4"
17
+ __version__ = "1.6.5"
18
18
 
19
19
  from .config import ConfigManager
20
20
  from .crumb import CrumbManager
@@ -238,8 +238,9 @@ class RubberDuck:
238
238
 
239
239
  if effective_command_mode:
240
240
  instruction = (
241
- "Return a single bash command that accomplishes the task. Unless user wants something els"
242
- "Do not include explanations or formatting other than the command itself."
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."
243
244
  )
244
245
  user_content = (
245
246
  f"{user_content}\n\n{instruction}" if user_content else instruction
@@ -288,10 +289,20 @@ class RubberDuck:
288
289
  )
289
290
 
290
291
  def _extract_command(self, content: str) -> str | None:
291
- lines = content.strip().splitlines()
292
- if not lines:
292
+ content = content.strip()
293
+ if not content:
293
294
  return None
294
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()
295
306
  command_lines: List[str] = []
296
307
  in_block = False
297
308
 
@@ -316,9 +327,20 @@ class RubberDuck:
316
327
 
317
328
  # Join all command lines with newlines for multi-line commands
318
329
  command = "\n".join(command_lines)
330
+ # Strip backticks if present
331
+ command = self._strip_backticks(command)
319
332
 
320
333
  return command or None
321
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
+
322
344
  async def list_models(self, host: str = "") -> list[str]:
323
345
  """List available Ollama models."""
324
346
  # Set the host temporarily for this operation
@@ -1095,11 +1117,12 @@ async def run_single_prompt(
1095
1117
  code: str | None = None,
1096
1118
  logger: ConversationLogger | None = None,
1097
1119
  suppress_suggestion: bool = False,
1120
+ command_mode: bool | None = None,
1098
1121
  ) -> AssistantResult:
1099
1122
  if logger:
1100
1123
  logger.log_user(prompt)
1101
1124
  try:
1102
- 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)
1103
1126
  except Exception as e:
1104
1127
  error_msg = str(e)
1105
1128
  if "unauthorized" in error_msg.lower() or "401" in error_msg:
@@ -1126,7 +1149,9 @@ async def run_single_prompt(
1126
1149
  raise
1127
1150
 
1128
1151
  content = result.content or "(No content returned.)"
1129
- console.print(content, style="dim", highlight=False)
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)
1130
1155
  if logger:
1131
1156
  logger.log_assistant(content, result.command)
1132
1157
  if result.command and not suppress_suggestion:
@@ -1145,6 +1170,51 @@ def copy_to_clipboard(text: str) -> bool:
1145
1170
  return False
1146
1171
 
1147
1172
 
1173
+ def check_for_updates() -> None:
1174
+ """Check PyPI for updates and notify user if a new version is available.
1175
+
1176
+ Checks once per day and caches the result to avoid excessive requests.
1177
+ """
1178
+ from datetime import datetime, timedelta
1179
+ import urllib.request
1180
+ import json
1181
+
1182
+ cache_file = HISTORY_DIR / "version_check_cache"
1183
+
1184
+ # Check if we've already checked today
1185
+ if cache_file.exists():
1186
+ try:
1187
+ last_check = datetime.fromtimestamp(cache_file.stat().st_mtime)
1188
+ if datetime.now() - last_check < timedelta(days=1):
1189
+ return # Already checked today
1190
+ except Exception:
1191
+ pass
1192
+
1193
+ try:
1194
+ # Query PyPI for latest version
1195
+ req = urllib.request.Request(
1196
+ "https://pypi.org/pypi/rubber-ducky/json",
1197
+ headers={"User-Agent": f"rubber-ducky/{__version__}"}
1198
+ )
1199
+
1200
+ with urllib.request.urlopen(req, timeout=3) as response:
1201
+ data = json.loads(response.read())
1202
+ latest_version = data["info"]["version"]
1203
+
1204
+ if latest_version != __version__:
1205
+ console.print(
1206
+ f"\n[dim]A new version is available: {latest_version} "
1207
+ f"(you have {__version__}). "
1208
+ f"Run: uv tool upgrade rubber-ducky[/dim]\n"
1209
+ )
1210
+
1211
+ # Update cache file timestamp
1212
+ cache_file.touch()
1213
+ except Exception:
1214
+ # Silently fail if no internet or PyPI is down
1215
+ pass
1216
+
1217
+
1148
1218
  def confirm(prompt: str, default: bool = False) -> bool:
1149
1219
  suffix = " [Y/n]: " if default else " [y/N]: "
1150
1220
  try:
@@ -1194,6 +1264,12 @@ async def ducky() -> None:
1194
1264
  action="store_true",
1195
1265
  help="Suppress startup messages and help text",
1196
1266
  )
1267
+ parser.add_argument(
1268
+ "--upgrade",
1269
+ "-u",
1270
+ action="store_true",
1271
+ help="Upgrade rubber-ducky to the latest version using uv",
1272
+ )
1197
1273
  parser.add_argument(
1198
1274
  "single_prompt",
1199
1275
  nargs="*",
@@ -1202,9 +1278,35 @@ async def ducky() -> None:
1202
1278
  )
1203
1279
  args = parser.parse_args()
1204
1280
 
1281
+ # Handle upgrade request immediately
1282
+ if args.upgrade:
1283
+ console.print("Upgrading rubber-ducky...", style="yellow")
1284
+ try:
1285
+ result = subprocess.run(
1286
+ ["uv", "tool", "upgrade", "rubber-ducky"],
1287
+ capture_output=True,
1288
+ text=True,
1289
+ check=True
1290
+ )
1291
+ console.print(result.stdout, style="dim")
1292
+ console.print("Upgrade complete!", style="green")
1293
+ except subprocess.CalledProcessError as e:
1294
+ console.print(f"Upgrade failed: {e.stderr}", style="red")
1295
+ except FileNotFoundError:
1296
+ console.print("uv not found. Please install uv to use --upgrade.", style="red")
1297
+ return
1298
+
1205
1299
  ensure_history_dir()
1206
1300
  logger = ConversationLogger(CONVERSATION_LOG_FILE)
1207
1301
 
1302
+ # Check for updates in background (non-blocking, runs once per day)
1303
+ # Skip if quiet mode, piped input, or single prompt (faster paths)
1304
+ check_piped = not sys.stdin.isatty()
1305
+ if not args.quiet and not args.single_prompt and not check_piped:
1306
+ # Run in a thread to not block startup
1307
+ import threading
1308
+ threading.Thread(target=check_for_updates, daemon=True).start()
1309
+
1208
1310
  # Load the last used model from config if no model is specified
1209
1311
  config_manager = ConfigManager()
1210
1312
  last_model, last_host = config_manager.get_last_model()
@@ -1235,20 +1337,35 @@ async def ducky() -> None:
1235
1337
 
1236
1338
  if piped_prompt is not None:
1237
1339
  if piped_prompt:
1238
- result = await run_single_prompt(
1239
- rubber_ducky, piped_prompt, code=code, logger=logger
1240
- )
1241
- if (
1242
- result.command
1243
- and sys.stdout.isatty()
1244
- and confirm("Run suggested command?")
1245
- ):
1246
- await run_shell_and_print(
1247
- rubber_ducky,
1248
- result.command,
1249
- logger=logger,
1250
- history=rubber_ducky.messages,
1340
+ # Check if user also provided a command-line prompt
1341
+ if args.single_prompt:
1342
+ # Combine piped content with user prompt
1343
+ # User prompt is the instruction, piped content is context
1344
+ user_prompt = " ".join(args.single_prompt)
1345
+ combined_prompt = (
1346
+ f"Context from stdin:\n```\n{piped_prompt}\n```\n\n"
1347
+ f"User request: {user_prompt}"
1251
1348
  )
1349
+ # Disable command_mode for this scenario - user wants an explanation, not a command
1350
+ result = await run_single_prompt(
1351
+ rubber_ducky, combined_prompt, code=code, logger=logger, command_mode=False
1352
+ )
1353
+ else:
1354
+ # Only piped input - proceed with command mode (default behavior)
1355
+ result = await run_single_prompt(
1356
+ rubber_ducky, piped_prompt, code=code, logger=logger
1357
+ )
1358
+ if (
1359
+ result.command
1360
+ and sys.stdout.isatty()
1361
+ and confirm("Run suggested command?")
1362
+ ):
1363
+ await run_shell_and_print(
1364
+ rubber_ducky,
1365
+ result.command,
1366
+ logger=logger,
1367
+ history=rubber_ducky.messages,
1368
+ )
1252
1369
  else:
1253
1370
  console.print("No input received from stdin.", style="yellow")
1254
1371
  return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rubber-ducky
3
- Version: 1.6.4
3
+ Version: 1.6.6
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=FEf-Xl7xMH-xV1fvYgV-ZgiNRURlS3tHIj7kJKsFurc,51977
4
+ ducky/ducky.py,sha256=pplx1lRnhpNoLn8prrfHL6FTZ0wAy0eRh-mN8lD7jxQ,56844
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.6.4.dist-info/licenses/LICENSE,sha256=gQ1rCmw18NqTk5GxG96F6vgyN70e1c4kcKUtWDwdNaE,1069
9
- rubber_ducky-1.6.4.dist-info/METADATA,sha256=wnYys-BUUyJqGnJdSIh1peeCJhMCOcNsBMoEYABlRk8,6638
10
- rubber_ducky-1.6.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- rubber_ducky-1.6.4.dist-info/entry_points.txt,sha256=WPnVUUNvWdMDcBlCo8JCzkLghGllMX5QVZyQghyq85Q,75
12
- rubber_ducky-1.6.4.dist-info/top_level.txt,sha256=hid_mDkugR6XIeravFKuzcRPpuN_ylN3ejC_06Fmnb4,15
13
- rubber_ducky-1.6.4.dist-info/RECORD,,
8
+ rubber_ducky-1.6.6.dist-info/licenses/LICENSE,sha256=gQ1rCmw18NqTk5GxG96F6vgyN70e1c4kcKUtWDwdNaE,1069
9
+ rubber_ducky-1.6.6.dist-info/METADATA,sha256=7Lo7fiBYqQgkaXhxtERGDm96C1PDH4nHV0zMwyuN3DY,6638
10
+ rubber_ducky-1.6.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ rubber_ducky-1.6.6.dist-info/entry_points.txt,sha256=WPnVUUNvWdMDcBlCo8JCzkLghGllMX5QVZyQghyq85Q,75
12
+ rubber_ducky-1.6.6.dist-info/top_level.txt,sha256=hid_mDkugR6XIeravFKuzcRPpuN_ylN3ejC_06Fmnb4,15
13
+ rubber_ducky-1.6.6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5