rubber-ducky 1.6.4__tar.gz → 1.6.6__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.4/rubber_ducky.egg-info → rubber_ducky-1.6.6}/PKG-INFO +1 -1
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/ducky/ducky.py +137 -20
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/pyproject.toml +1 -1
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6/rubber_ducky.egg-info}/PKG-INFO +1 -1
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/LICENSE +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/MANIFEST.in +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/README.md +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/ducky/__init__.py +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/ducky/config.py +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/ducky/crumb.py +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/examples/POLLING_USER_GUIDE.md +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/examples/mock-logs/info.txt +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/examples/mock-logs/mock-logs.sh +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/rubber_ducky.egg-info/SOURCES.txt +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/rubber_ducky.egg-info/dependency_links.txt +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/rubber_ducky.egg-info/entry_points.txt +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/rubber_ducky.egg-info/requires.txt +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/rubber_ducky.egg-info/top_level.txt +0 -0
- {rubber_ducky-1.6.4 → rubber_ducky-1.6.6}/setup.cfg +0 -0
|
@@ -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.
|
|
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
|
|
242
|
-
"
|
|
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
|
-
|
|
292
|
-
if not
|
|
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
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
|
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
|