rubber-ducky 1.1.5__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/__init__.py CHANGED
@@ -1 +1 @@
1
- from .ducky import ducky
1
+ from .ducky import ducky
ducky/ducky.py CHANGED
@@ -1,40 +1,429 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import asyncio
5
+ import json
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from textwrap import dedent
11
+ from typing import Any, Dict, List
12
+
3
13
  from ollama import AsyncClient
14
+ from contextlib import nullcontext
15
+
16
+ try: # prompt_toolkit is optional at runtime
17
+ from prompt_toolkit import PromptSession
18
+ from prompt_toolkit.history import FileHistory
19
+ from prompt_toolkit.key_binding import KeyBindings
20
+ from prompt_toolkit.patch_stdout import patch_stdout
21
+ except ImportError: # pragma: no cover - fallback mode
22
+ PromptSession = None # type: ignore[assignment]
23
+ FileHistory = None # type: ignore[assignment]
24
+ KeyBindings = None # type: ignore[assignment]
25
+
26
+ def patch_stdout() -> nullcontext:
27
+ return nullcontext()
28
+
29
+
30
+ from rich.console import Console
31
+
32
+
33
+ @dataclass
34
+ class AssistantResult:
35
+ content: str
36
+ command: str | None
37
+ thinking: str | None = None
38
+
39
+
40
+ @dataclass
41
+ class ShellResult:
42
+ command: str
43
+ stdout: str
44
+ stderr: str
45
+ returncode: int
46
+
47
+
48
+ HISTORY_DIR = Path.home() / ".ducky"
49
+ PROMPT_HISTORY_FILE = HISTORY_DIR / "prompt_history"
50
+ CONVERSATION_LOG_FILE = HISTORY_DIR / "conversation.log"
51
+ console = Console()
52
+
53
+
54
+ def ensure_history_dir() -> Path:
55
+ HISTORY_DIR.mkdir(parents=True, exist_ok=True)
56
+ return HISTORY_DIR
57
+
58
+
59
+ class ConversationLogger:
60
+ def __init__(self, log_path: Path) -> None:
61
+ self.log_path = log_path
62
+
63
+ def log_user(self, content: str) -> None:
64
+ if content.strip():
65
+ self._append({"role": "user", "content": content})
66
+
67
+ def log_assistant(self, content: str, command: str | None) -> None:
68
+ entry: Dict[str, Any] = {"role": "assistant", "content": content}
69
+ if command:
70
+ entry["suggested_command"] = command
71
+ self._append(entry)
72
+
73
+ def log_shell(self, result: ShellResult) -> None:
74
+ self._append(
75
+ {
76
+ "role": "shell",
77
+ "command": result.command,
78
+ "stdout": result.stdout,
79
+ "stderr": result.stderr,
80
+ "returncode": result.returncode,
81
+ }
82
+ )
83
+
84
+ def _append(self, entry: Dict[str, Any]) -> None:
85
+ entry["timestamp"] = datetime.utcnow().isoformat()
86
+ with self.log_path.open("a", encoding="utf-8") as handle:
87
+ handle.write(json.dumps(entry, ensure_ascii=False))
88
+ handle.write("\n")
89
+
90
+
91
+ def print_shell_result(result: ShellResult) -> None:
92
+ printed = False
93
+ if result.stdout.strip():
94
+ console.print(result.stdout.rstrip(), highlight=False)
95
+ printed = True
96
+ if result.stderr.strip():
97
+ if printed:
98
+ console.print()
99
+ console.print("[stderr]", style="bold red")
100
+ console.print(result.stderr.rstrip(), style="red", highlight=False)
101
+ printed = True
102
+ if result.returncode != 0 or not printed:
103
+ suffix = (
104
+ f"(exit status {result.returncode})"
105
+ if result.returncode != 0
106
+ else "(command produced no output)"
107
+ )
108
+ console.print(suffix, style="yellow")
109
+
110
+
111
+ async def run_shell_and_print(
112
+ assistant: RubberDuck,
113
+ command: str,
114
+ logger: ConversationLogger | None = None,
115
+ history: list[dict[str, str]] | None = None,
116
+ ) -> None:
117
+ if not command:
118
+ console.print("No command provided.", style="yellow")
119
+ return
120
+ console.print(f"$ {command}", style="bold magenta")
121
+ result = await assistant.run_shell_command(command)
122
+ print_shell_result(result)
123
+ if logger:
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)})
4
137
 
5
138
 
6
139
  class RubberDuck:
7
- def __init__(self, model: str, quick: bool = False) -> None:
8
- self.system_prompt = """You are a pair progamming tool called Ducky or RubberDucky to help developers debug, think through design, and write code.
9
- Help the user think through their approach and provide feedback on the code. Think step by step and ask clarifying questions if needed.
10
- If asked """
140
+ def __init__(
141
+ self, model: str, quick: bool = False, command_mode: bool = False
142
+ ) -> None:
143
+ self.system_prompt = dedent(
144
+ """
145
+ You are a pair programming tool called Ducky or RubberDucky to help
146
+ developers debug, think through design decisions, and write code.
147
+ Help the user reason about their approach and provide feedback on
148
+ the code. Think step by step and ask clarifying questions if
149
+ needed.
150
+
151
+ When the user provides git status output or similar multi-line terminal
152
+ output, provide a single comprehensive response that addresses all the
153
+ changes rather than responding to each line individually.
154
+ """
155
+ ).strip()
11
156
  self.client = AsyncClient()
12
157
  self.model = model
13
158
  self.quick = quick
159
+ self.command_mode = command_mode
160
+ self.messages: List[Dict[str, str]] = [
161
+ {"role": "system", "content": self.system_prompt}
162
+ ]
163
+ self.last_thinking: str | None = None
14
164
 
15
- async def call_llm(self, prompt: str | None = None) -> None:
16
- chain = False if prompt else True
165
+ async def send_prompt(
166
+ self, prompt: str | None = None, code: str | None = None
167
+ ) -> AssistantResult:
168
+ user_content = (prompt or "").strip()
17
169
 
18
- if prompt is None:
19
- prompt = input("\nEnter your prompt (or press Enter for default review): ")
170
+ if code:
171
+ user_content = f"{user_content}\n\n{code}" if user_content else code
20
172
 
21
- if self.quick:
22
- prompt += ". Return a command and be extremely concise"
173
+ if self.quick and user_content:
174
+ user_content += ". Return a command and be extremely concise"
23
175
 
24
- responses = [self.system_prompt]
25
- while True:
26
- context_prompt = "\n".join(responses) + "\n" + prompt
27
- stream = await self.client.generate(model=self.model, prompt=context_prompt, stream=True)
28
- response_text = ""
29
- async for chunk in stream:
30
- if 'response' in chunk:
31
- print(chunk['response'], end='', flush=True)
32
- response_text += chunk['response']
33
- print()
34
- responses.append(response_text)
35
- if not chain:
176
+ if self.command_mode:
177
+ instruction = (
178
+ "Return a single bash command that accomplishes the task. "
179
+ "Do not include explanations or formatting other than the command itself."
180
+ )
181
+ user_content = (
182
+ f"{user_content}\n\n{instruction}" if user_content else instruction
183
+ )
184
+
185
+ user_message: Dict[str, str] = {"role": "user", "content": user_content}
186
+ self.messages.append(user_message)
187
+
188
+ response = await self.client.chat(
189
+ model=self.model,
190
+ messages=self.messages,
191
+ stream=False,
192
+ think=True,
193
+ )
194
+
195
+ assistant_message: Any | None = response.message
196
+ if assistant_message is None:
197
+ raise RuntimeError("No response received from the model.")
198
+
199
+ content = getattr(assistant_message, "content", "") or ""
200
+ thinking = getattr(assistant_message, "thinking", None)
201
+
202
+ self.messages.append({"role": "assistant", "content": content})
203
+
204
+ if thinking:
205
+ self.last_thinking = thinking
206
+
207
+ command = self._extract_command(content) if self.command_mode else None
208
+
209
+ return AssistantResult(content=content, command=command, thinking=thinking)
210
+
211
+ async def run_shell_command(self, command: str) -> ShellResult:
212
+ process = await asyncio.create_subprocess_shell(
213
+ command,
214
+ stdout=asyncio.subprocess.PIPE,
215
+ stderr=asyncio.subprocess.PIPE,
216
+ )
217
+ stdout, stderr = await process.communicate()
218
+ return ShellResult(
219
+ command=command,
220
+ stdout=stdout.decode(errors="replace"),
221
+ stderr=stderr.decode(errors="replace"),
222
+ returncode=process.returncode or 0,
223
+ )
224
+
225
+ def _extract_command(self, content: str) -> str | None:
226
+ lines = content.strip().splitlines()
227
+ if not lines:
228
+ return None
229
+
230
+ command_lines: List[str] = []
231
+
232
+ in_block = False
233
+ for line in lines:
234
+ stripped = line.strip()
235
+ if stripped.startswith("```"):
236
+ if in_block:
237
+ break
238
+ in_block = True
239
+ continue
240
+ if in_block:
241
+ if stripped:
242
+ command_lines = [stripped]
243
+ break
244
+ continue
245
+ if stripped:
246
+ command_lines = [stripped]
36
247
  break
37
- prompt = input("\n>> ")
248
+
249
+ if not command_lines:
250
+ return None
251
+
252
+ command = command_lines[0]
253
+ first_semicolon = command.find(";")
254
+ if first_semicolon != -1:
255
+ command = command[:first_semicolon].strip()
256
+
257
+ return command or None
258
+
259
+
260
+ class InlineInterface:
261
+ def __init__(
262
+ self,
263
+ assistant: RubberDuck,
264
+ logger: ConversationLogger | None = None,
265
+ code: str | None = None,
266
+ ) -> None:
267
+ ensure_history_dir()
268
+ self.assistant = assistant
269
+ self.logger = logger
270
+ self.last_command: str | None = None
271
+ self.code = code
272
+ self._code_sent = False
273
+ self.last_shell_output: str | None = None
274
+ self.pending_command: str | None = None
275
+ self.session: PromptSession | None = None
276
+
277
+ if (
278
+ PromptSession is not None
279
+ and FileHistory is not None
280
+ and KeyBindings is not None
281
+ ):
282
+ self.session = PromptSession(
283
+ message=">> ",
284
+ multiline=True,
285
+ history=FileHistory(str(PROMPT_HISTORY_FILE)),
286
+ key_bindings=self._create_key_bindings(),
287
+ )
288
+
289
+ def _create_key_bindings(self) -> KeyBindings | None:
290
+ if KeyBindings is None: # pragma: no cover - fallback mode
291
+ return None
292
+
293
+ kb = KeyBindings()
294
+
295
+ @kb.add("enter")
296
+ def _(event) -> None:
297
+ buffer = event.current_buffer
298
+ buffer.validate_and_handle()
299
+
300
+ @kb.add("c-j")
301
+ def _(event) -> None:
302
+ event.current_buffer.insert_text("\n")
303
+
304
+ @kb.add("c-r")
305
+ def _(event) -> None:
306
+ event.app.exit(result="__RUN_LAST__")
307
+
308
+ return kb
309
+
310
+ async def run(self) -> None:
311
+ if self.session is None:
312
+ console.print(
313
+ "prompt_toolkit not installed. Falling back to basic input (no history/shortcuts).",
314
+ style="yellow",
315
+ )
316
+ await self._run_basic_loop()
317
+ return
318
+
319
+ console.print(
320
+ "Enter submits • empty Enter reruns the last suggested command (or explains the last shell output) • '!cmd' runs shell • Ctrl+D exits",
321
+ style="dim",
322
+ )
323
+ while True:
324
+ try:
325
+ with patch_stdout():
326
+ text = await self.session.prompt_async()
327
+ except EOFError:
328
+ console.print()
329
+ console.print("Exiting.", style="dim")
330
+ return
331
+ except KeyboardInterrupt:
332
+ console.print()
333
+ console.print("Interrupted. Press Ctrl+D to exit.", style="yellow")
334
+ continue
335
+
336
+ if text == "__RUN_LAST__":
337
+ await self._run_last_command()
338
+ continue
339
+
340
+ await self._process_text(text)
341
+
342
+ async def _run_last_command(self) -> None:
343
+ if not self.last_command:
344
+ console.print("No suggested command available yet.", style="yellow")
345
+ return
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
355
+
356
+ async def _process_text(self, text: str) -> None:
357
+ stripped = text.strip()
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")
366
+ return
367
+
368
+ if stripped.lower() in {":run", "/run"}:
369
+ await self._run_last_command()
370
+ return
371
+
372
+ if stripped.startswith("!"):
373
+ await run_shell_and_print(
374
+ self.assistant,
375
+ stripped[1:].strip(),
376
+ logger=self.logger,
377
+ history=self.assistant.messages,
378
+ )
379
+ self.last_shell_output = True
380
+ self.pending_command = None
381
+ return
382
+
383
+ result = await run_single_prompt(
384
+ self.assistant,
385
+ stripped,
386
+ code=self.code if not self._code_sent else None,
387
+ logger=self.logger,
388
+ )
389
+ if self.code and not self._code_sent:
390
+ self._code_sent = True
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
412
+
413
+ async def _run_basic_loop(self) -> None: # pragma: no cover - fallback path
414
+ while True:
415
+ try:
416
+ text = await asyncio.to_thread(input, ">> ")
417
+ except EOFError:
418
+ console.print()
419
+ console.print("Exiting.", style="dim")
420
+ return
421
+ except KeyboardInterrupt:
422
+ console.print()
423
+ console.print("Interrupted. Press Ctrl+D to exit.", style="yellow")
424
+ continue
425
+
426
+ await self._process_text(text)
38
427
 
39
428
 
40
429
  def read_files_from_dir(directory: str) -> str:
@@ -43,51 +432,102 @@ def read_files_from_dir(directory: str) -> str:
43
432
  files = os.listdir(directory)
44
433
  code = ""
45
434
  for file in files:
46
- code += open(directory + "/" + file).read()
435
+ full_path = f"{directory}/{file}"
436
+ if not os.path.isfile(full_path):
437
+ continue
438
+ with open(full_path, "r", encoding="utf-8", errors="ignore") as handle:
439
+ code += handle.read()
47
440
  return code
48
441
 
49
442
 
443
+ async def run_single_prompt(
444
+ rubber_ducky: RubberDuck,
445
+ prompt: str,
446
+ code: str | None = None,
447
+ logger: ConversationLogger | None = None,
448
+ suppress_suggestion: bool = False,
449
+ ) -> AssistantResult:
450
+ if logger:
451
+ logger.log_user(prompt)
452
+ result = await rubber_ducky.send_prompt(prompt=prompt, code=code)
453
+ content = result.content or "(No content returned.)"
454
+ console.print(content, style="green", highlight=False)
455
+ if logger:
456
+ logger.log_assistant(content, result.command)
457
+ if result.command and not suppress_suggestion:
458
+ console.print("\nSuggested command:", style="cyan", highlight=False)
459
+ console.print(result.command, style="bold cyan", highlight=False)
460
+ return result
461
+
462
+
463
+ def confirm(prompt: str, default: bool = False) -> bool:
464
+ suffix = " [Y/n]: " if default else " [y/N]: "
465
+ try:
466
+ choice = input(prompt + suffix)
467
+ except EOFError:
468
+ return default
469
+ choice = choice.strip().lower()
470
+ if not choice:
471
+ return default
472
+ return choice in {"y", "yes"}
473
+
474
+
475
+ async def interactive_session(
476
+ rubber_ducky: RubberDuck,
477
+ logger: ConversationLogger | None = None,
478
+ code: str | None = None,
479
+ ) -> None:
480
+ ui = InlineInterface(rubber_ducky, logger=logger, code=code)
481
+ await ui.run()
482
+
483
+
50
484
  async def ducky() -> None:
51
485
  parser = argparse.ArgumentParser()
52
- parser.add_argument("question", nargs="*", help="Direct question to ask", default=None)
53
- parser.add_argument("--prompt", "-p", help="Custom prompt to be used", default=None)
54
- parser.add_argument("--file", "-f", help="The file to be processed", default=None)
55
- parser.add_argument("--directory", "-d", help="The directory to be processed", default=None)
56
- parser.add_argument("--quick", "-q", help="Quick mode", default=False)
57
486
  parser.add_argument(
58
- "--chain",
59
- "-c",
60
- help="Chain the output of the previous command to the next command",
61
- action="store_true",
62
- default=False,
487
+ "--directory", "-d", help="The directory to be processed", default=None
63
488
  )
64
489
  parser.add_argument(
65
- "--model", "-m", help="The model to be used", default="qwen2.5-coder"
490
+ "--model", "-m", help="The model to be used", default="qwen3-coder:480b-cloud"
66
491
  )
67
492
  args, _ = parser.parse_known_args()
68
493
 
69
- rubber_ducky = RubberDuck(model=args.model, quick=args.quick)
494
+ ensure_history_dir()
495
+ logger = ConversationLogger(CONVERSATION_LOG_FILE)
496
+ rubber_ducky = RubberDuck(model=args.model, quick=False, command_mode=True)
70
497
 
71
- # Handle direct question from CLI
72
- if args.question:
73
- question = " ".join(args.question)
74
- await rubber_ducky.call_llm(prompt=question)
75
- return
498
+ code = read_files_from_dir(args.directory) if args.directory else None
76
499
 
77
- # Handle interactive mode (no file/directory specified)
78
- if args.file is None and args.directory is None:
79
- await rubber_ducky.call_llm(prompt=args.prompt)
80
- return
500
+ piped_prompt: str | None = None
501
+ if not sys.stdin.isatty():
502
+ piped_prompt = sys.stdin.read()
503
+ piped_prompt = piped_prompt.strip() or None
81
504
 
82
- # Get code from file or directory
83
- code = (open(args.file).read() if args.file
84
- else read_files_from_dir(args.directory))
505
+ if piped_prompt is not None:
506
+ if piped_prompt:
507
+ result = await run_single_prompt(
508
+ rubber_ducky, piped_prompt, code=code, logger=logger
509
+ )
510
+ if (
511
+ result.command
512
+ and sys.stdout.isatty()
513
+ and confirm("Run suggested command?")
514
+ ):
515
+ await run_shell_and_print(
516
+ rubber_ducky,
517
+ result.command,
518
+ logger=logger,
519
+ history=rubber_ducky.messages,
520
+ )
521
+ else:
522
+ console.print("No input received from stdin.", style="yellow")
523
+ return
85
524
 
86
- await rubber_ducky.call_llm(code=code, prompt=args.prompt)
525
+ await interactive_session(rubber_ducky, logger=logger, code=code)
87
526
 
88
527
 
89
- def main():
528
+ def main() -> None:
90
529
  asyncio.run(ducky())
91
530
 
531
+
92
532
  if __name__ == "__main__":
93
533
  main()
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: rubber-ducky
3
+ Version: 1.2.1
4
+ Summary: Quick CLI do-it-all tool. Use natural language to spit out bash commands
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: colorama>=0.4.6
9
+ Requires-Dist: fastapi>=0.115.11
10
+ Requires-Dist: ollama>=0.6.0
11
+ Requires-Dist: openai>=1.60.2
12
+ Requires-Dist: prompt-toolkit>=3.0.48
13
+ Requires-Dist: rich>=13.9.4
14
+ Requires-Dist: termcolor>=2.5.0
15
+ Dynamic: license-file
16
+
17
+ # Rubber Ducky
18
+
19
+ Rubber Ducky is an inline terminal companion that turns natural language prompts into runnable shell commands. Paste multi-line context, get a suggested command, and run it without leaving your terminal.
20
+
21
+ ## Quick Start
22
+
23
+ | Action | Command |
24
+ | --- | --- |
25
+ | Install globally | `uv tool install rubber-ducky` |
26
+ | Run once | `uvx rubber-ducky -- --help` |
27
+ | Local install | `uv pip install rubber-ducky` |
28
+
29
+ Requirements:
30
+ - [Ollama](https://ollama.com) running locally
31
+ - Model available via Ollama (default: `qwen3-coder:480b-cloud`, install with `ollama pull qwen3-coder:480b-cloud`)
32
+
33
+ ## Usage
34
+
35
+ ```
36
+ ducky # interactive inline session
37
+ ducky --directory src # preload code from a directory
38
+ ducky --model llama3 # use a different Ollama model
39
+ ```
40
+
41
+ Both `ducky` and `rubber-ducky` executables map to the same CLI, so `uvx rubber-ducky -- <args>` works as well.
42
+
43
+ ### Inline Session (default)
44
+
45
+ Launching `ducky` with no arguments opens the inline interface:
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
+ - **Ctrl+R** re-runs the last suggested command.
48
+ - Prefix any line with **`!`** (e.g., `!ls -la`) to run a shell command immediately.
49
+ - Arrow keys browse prompt history, backed by `~/.ducky/prompt_history`.
50
+ - Every prompt, assistant response, and executed command is logged to `~/.ducky/conversation.log`.
51
+ - Press **Ctrl+D** on an empty line to exit.
52
+ - Non-interactive runs such as `cat prompt.txt | ducky` print one response (and suggested command) before exiting; if a TTY is available you'll be asked whether to run the suggested command immediately.
53
+ - If `prompt_toolkit` is unavailable in your environment, Rubber Ducky falls back to a basic input loop (no history or shortcuts); install `prompt-toolkit>=3.0.48` to unlock the richer UI.
54
+
55
+ `ducky --directory <path>` streams the contents of the provided directory to the assistant the next time you submit a prompt (the directory is read once at startup).
56
+
57
+ ## Development (uv)
58
+
59
+ ```
60
+ uv sync
61
+ uv run ducky --help
62
+ ```
63
+
64
+ `uv sync` creates a virtual environment and installs dependencies defined in `pyproject.toml` / `uv.lock`.
65
+
66
+ ## Telemetry & Storage
67
+
68
+ Rubber Ducky stores:
69
+ - `~/.ducky/prompt_history`: readline-compatible history file.
70
+ - `~/.ducky/conversation.log`: JSON lines with timestamps for prompts, assistant messages, and shell executions.
71
+
72
+ No other telemetry is collected; delete the directory if you want a fresh slate.
@@ -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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  ducky = ducky.ducky:main
3
+ rubber-ducky = ducky.ducky:main
@@ -1,71 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: rubber-ducky
3
- Version: 1.1.5
4
- Summary: AI Companion for Pair Programming
5
- Home-page: https://github.com/ParthSareen/ducky
6
- Author: Parth Sareen
7
- Author-email: psareen@uwaterloo.ca
8
- License: MIT
9
- Description-Content-Type: text/markdown
10
- License-File: LICENSE
11
- Requires-Dist: ollama
12
- Dynamic: author
13
- Dynamic: author-email
14
- Dynamic: description
15
- Dynamic: description-content-type
16
- Dynamic: home-page
17
- Dynamic: license
18
- Dynamic: requires-dist
19
- Dynamic: summary
20
-
21
- # rubber ducky
22
- <p align="center">
23
- <img src="ducky_img.webp" alt="Ducky Image" width="200" height="200">
24
- </p>
25
-
26
- ## tl;dr
27
- - `pip install rubber-ducky`
28
- - Install ollama
29
- - `ollama pull codellama` (first time and then you can just have application in background)
30
- - There are probably other dependencies which I forgot to put in setup.py sorry in advance.
31
- - Run with `ducky <path>` or `ducky <question>`
32
-
33
- ## Dependencies
34
-
35
- You will need Ollama installed on your machine. The model I use for this project is `codellama`.
36
-
37
- For the first installation you can run `ollama pull codellama` and it should pull the necessary binaries for you.
38
-
39
- Ollama is also great because it'll spin up a server which can run in the background and can even do automatic model switching as long as you have it installed.
40
-
41
- ## Usage
42
-
43
- Install through [pypi](https://pypi.org/project/rubber-ducky/):
44
-
45
- `pip install rubber-ducky` .
46
-
47
- ### Simple run
48
- `ducky`
49
-
50
- or
51
-
52
- `ducky <question>`
53
-
54
- or
55
-
56
- `ducky -f <path>`
57
-
58
-
59
- ### All options
60
- `ducky --file <path> --prompt <prompt> --directory <directory> --chain --model <model>`
61
-
62
- Where:
63
- - `--prompt` or `-p`: Custom prompt to be used
64
- - `--file` or `-f`: The file to be processed
65
- - `--directory` or `-d`: The directory to be processed
66
- - `--chain` or `-c`: Chain the output of the previous command to the next command
67
- - `--model` or `-m`: The model to be used (default is "codellama")
68
-
69
-
70
- ## Example output
71
- ![Screenshot of ducky](image.png)
@@ -1,8 +0,0 @@
1
- ducky/__init__.py,sha256=_7imP8Jc2SIapn4fzGkspmKvqxPTFDcDJWHZ_o_MnlE,24
2
- ducky/ducky.py,sha256=4F0vB-HWa96OKlLyKxB2NY4a0GmGTxThqnjlQLsRWgE,3245
3
- rubber_ducky-1.1.5.dist-info/LICENSE,sha256=gQ1rCmw18NqTk5GxG96F6vgyN70e1c4kcKUtWDwdNaE,1069
4
- rubber_ducky-1.1.5.dist-info/METADATA,sha256=fe4_S5vJE_XJUzto38xDfg2fGbQJYXQ3M4OBBZY0z4g,1905
5
- rubber_ducky-1.1.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
6
- rubber_ducky-1.1.5.dist-info/entry_points.txt,sha256=LPndtj8UqEWtwYApv5LJJniH4FUrsriOqV2LA1X_UPQ,43
7
- rubber_ducky-1.1.5.dist-info/top_level.txt,sha256=4Q75MONDNPpQ3o17bTu7RFuKwFhTIRzlXP3_LDWQQ30,6
8
- rubber_ducky-1.1.5.dist-info/RECORD,,