dbagent-cli 0.11.4__tar.gz → 0.11.5__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.
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/PKG-INFO +1 -1
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/__init__.py +1 -1
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/cli.py +189 -12
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/chat_screen.py +8 -3
- dbagent_cli-0.11.5/dbagent/ui/clipboard.py +280 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/PKG-INFO +1 -1
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/SOURCES.txt +2 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/pyproject.toml +1 -1
- dbagent_cli-0.11.5/tests/test_clipboard.py +120 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/README.md +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/confidence.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/conversation.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/doctor.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/error_classifier.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/generator.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/pipeline.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/recommender.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/validator.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/benchmark.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/config.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/connectors/base.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/connectors/factory.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/connectors/mongo.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/connectors/relational.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/errors.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/__init__.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/cache.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/changeset.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/cost.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/prompt_queue.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/recovery.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/sandbox.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/streaming.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/anthropic_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/auto_setup.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/base.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/deepseek_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/factory.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/gemini_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/groq_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/mistral_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/mock_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/ollama_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/openai_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/openrouter_provider.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/plugins.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/sample_db.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/cache.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/formatter.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/graph.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/ground_truth.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/models.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/retriever.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/selector.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/__init__.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/audit.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/environment.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/masking.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/modes.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/rbac.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/semantic/__init__.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/semantic/dictionary.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/semantic/knowledge.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/semantic/templates.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/sql/__init__.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/sql/optimizer.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/sql/validator.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/console.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/grid_window.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/live_streamer.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/viewer.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/visualizer.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/dependency_links.txt +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/entry_points.txt +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/requires.txt +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/top_level.txt +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/setup.cfg +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_benchmarks.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_cache.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_cli.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_connectors.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_demo_and_sample_db.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_direct_sql.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_doctor_and_viz.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_formats_and_exit_codes.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_generator.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_graph.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_ground_truth_sql_validation.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_knowledge.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_multi_turn_conversation.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_plugins.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_prompt_queue.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_prompt_queue_and_sql.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_providers.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_recovery.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_safety.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_sandbox.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_schema.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_security.py +0 -0
- {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_semantic.py +0 -0
|
@@ -27,6 +27,7 @@ from prompt_toolkit import PromptSession, HTML
|
|
|
27
27
|
from prompt_toolkit.history import FileHistory
|
|
28
28
|
from prompt_toolkit.completion import WordCompleter
|
|
29
29
|
from prompt_toolkit.styles import Style
|
|
30
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
30
31
|
|
|
31
32
|
from dbagent import __version__, __author__
|
|
32
33
|
from dbagent.config import ConfigManager
|
|
@@ -73,6 +74,7 @@ from dbagent.ui.console import (
|
|
|
73
74
|
)
|
|
74
75
|
from dbagent.ui.viewer import SchemaViewer
|
|
75
76
|
from dbagent.ui.grid_window import open_desktop_grid_window, open_browser_grid_window
|
|
77
|
+
from dbagent.ui.clipboard import ClipboardManager
|
|
76
78
|
from dbagent.ui.chat_screen import (
|
|
77
79
|
clear_screen,
|
|
78
80
|
render_chat_header,
|
|
@@ -625,6 +627,45 @@ def ask_command(
|
|
|
625
627
|
raise typer.Exit(1)
|
|
626
628
|
|
|
627
629
|
|
|
630
|
+
def open_multiline_paste_session() -> Optional[str]:
|
|
631
|
+
"""
|
|
632
|
+
Opens an interactive multiline editing / paste buffer for long SQL scripts or complex prompts.
|
|
633
|
+
Submit: Type ';;' on an empty line, or press Esc+Enter / Alt+Enter.
|
|
634
|
+
Cancel: Press Ctrl+C.
|
|
635
|
+
"""
|
|
636
|
+
console.print("\n[bold cyan]📋 Multiline Paste & Edit Mode[/bold cyan]")
|
|
637
|
+
console.print("[dim]Paste your multiline SQL or instructions below.[/dim]")
|
|
638
|
+
console.print("[dim]Submit: Type [bold white];;[/bold white] on a new line or press [bold white]Esc+Enter[/bold white] · Cancel: [bold white]Ctrl+C[/bold white]\n[/dim]")
|
|
639
|
+
|
|
640
|
+
kb = KeyBindings()
|
|
641
|
+
|
|
642
|
+
@kb.add("escape", "enter")
|
|
643
|
+
def _submit(event):
|
|
644
|
+
event.current_buffer.validate_and_handle()
|
|
645
|
+
|
|
646
|
+
try:
|
|
647
|
+
multi_style = Style.from_dict({
|
|
648
|
+
"prompt": "#06b6d4 bold",
|
|
649
|
+
})
|
|
650
|
+
multi_session = PromptSession(
|
|
651
|
+
key_bindings=kb,
|
|
652
|
+
multiline=True,
|
|
653
|
+
enable_bracketed_paste=True,
|
|
654
|
+
style=multi_style,
|
|
655
|
+
)
|
|
656
|
+
text = multi_session.prompt(
|
|
657
|
+
HTML("<ansicyan><b>... </b></ansicyan>"),
|
|
658
|
+
prompt_continuation=lambda width, line_number, is_soft_wrap: "... ",
|
|
659
|
+
)
|
|
660
|
+
clean = text.strip()
|
|
661
|
+
if clean.endswith(";;"):
|
|
662
|
+
clean = clean[:-2].strip()
|
|
663
|
+
return clean if clean else None
|
|
664
|
+
except (KeyboardInterrupt, EOFError):
|
|
665
|
+
console.print("[dim italic]Multiline input cancelled.[/dim italic]\n")
|
|
666
|
+
return None
|
|
667
|
+
|
|
668
|
+
|
|
628
669
|
# ---- chat (interactive REPL) ----
|
|
629
670
|
|
|
630
671
|
@app.command(name="chat")
|
|
@@ -686,6 +727,7 @@ def chat_command(
|
|
|
686
727
|
chat_completer = WordCompleter([
|
|
687
728
|
":help", ":tables", ":schema", ":dt", ":table", ":fk", ":path", ":join",
|
|
688
729
|
":doctor", ":mode", ":changes", ":provider", ":model", ":models",
|
|
730
|
+
":copy", ":cp", ":c", ":paste", ":p", ":multiline", ":paste-mode", ":m",
|
|
689
731
|
":page", ":card", ":wide", ":scroll", ":window", ":grid", ":web",
|
|
690
732
|
":explain", ":history", ":queue", ":auto", ":noauto", ":run", ":clear", ":export",
|
|
691
733
|
":exit", ":quit", ":q",
|
|
@@ -697,16 +739,29 @@ def chat_command(
|
|
|
697
739
|
"bottom-toolbar.cyan": "#06b6d4 bold",
|
|
698
740
|
"bottom-toolbar.yellow": "#f59e0b bold",
|
|
699
741
|
})
|
|
742
|
+
|
|
743
|
+
kb = KeyBindings()
|
|
744
|
+
|
|
745
|
+
@kb.add("escape", "v")
|
|
746
|
+
def _kb_paste_clipboard(event):
|
|
747
|
+
"""Alt+V / Esc, V shortcut: paste clipboard text into current buffer."""
|
|
748
|
+
text, _ = ClipboardManager.get_text()
|
|
749
|
+
if text:
|
|
750
|
+
event.current_buffer.insert_text(text)
|
|
751
|
+
|
|
700
752
|
session = PromptSession(
|
|
701
753
|
history=FileHistory(str(history_file)),
|
|
702
754
|
completer=chat_completer,
|
|
703
755
|
style=chat_style,
|
|
756
|
+
key_bindings=kb,
|
|
757
|
+
enable_bracketed_paste=True,
|
|
704
758
|
)
|
|
705
759
|
|
|
706
760
|
auto_execute = True
|
|
707
761
|
conversation_ctx = ConversationContext()
|
|
708
762
|
query_history: List[PipelineResult] = []
|
|
709
763
|
last_result: Optional[PipelineResult] = None
|
|
764
|
+
last_user_prompt: str = ""
|
|
710
765
|
|
|
711
766
|
from dbagent.execution.prompt_queue import PromptQueueManager
|
|
712
767
|
prompt_queue = PromptQueueManager(console=console)
|
|
@@ -785,6 +840,9 @@ def chat_command(
|
|
|
785
840
|
cmd_table.add_row(":learn <rule>", "Teach DB-Agent custom domain rules & mappings (:learn topic = rule)")
|
|
786
841
|
cmd_table.add_row(":memory, :learned", "View all learned domain rules & relationship memory")
|
|
787
842
|
cmd_table.add_row(":forget <id|topic>", "Remove a learned rule from persistent memory")
|
|
843
|
+
cmd_table.add_row(":copy [format]", "Copy SQL, CSV, TSV (Excel), JSON, Markdown or INSERTs to clipboard")
|
|
844
|
+
cmd_table.add_row(":paste, :p", "Paste and execute SQL or prompt directly from system clipboard")
|
|
845
|
+
cmd_table.add_row(":multiline, :m", "Open multiline paste mode for long SQL blocks (Esc+Enter / ;;)")
|
|
788
846
|
cmd_table.add_row(":mode <name>", "Switch safety mode (SAFE, CONFIRM, ADMIN, READ_ONLY)")
|
|
789
847
|
cmd_table.add_row(":history", "View session query history, execution timing & row counts")
|
|
790
848
|
cmd_table.add_row(":queue, :q-list", "View queued in-line prompts (:queue clear to reset)")
|
|
@@ -1124,7 +1182,123 @@ def chat_command(
|
|
|
1124
1182
|
)
|
|
1125
1183
|
continue
|
|
1126
1184
|
|
|
1127
|
-
if cmd_name in [":
|
|
1185
|
+
if cmd_name in [":copy", ":cp", ":c"]:
|
|
1186
|
+
target = cmd_arg.strip().lower() if cmd_arg else "sql"
|
|
1187
|
+
if target in ["sql", "query", "code"]:
|
|
1188
|
+
if not last_result or not last_result.sql:
|
|
1189
|
+
print_warning("No generated SQL query to copy.")
|
|
1190
|
+
continue
|
|
1191
|
+
ok, msg = ClipboardManager.copy_text(last_result.sql.strip())
|
|
1192
|
+
if ok:
|
|
1193
|
+
print_success(f"Copied SQL query to clipboard! ({len(last_result.sql.strip())} chars)")
|
|
1194
|
+
else:
|
|
1195
|
+
print_error(f"Clipboard copy failed: {msg}")
|
|
1196
|
+
elif target in ["csv", "comma"]:
|
|
1197
|
+
if not last_result or not last_result.rows:
|
|
1198
|
+
print_warning("No query result rows to copy.")
|
|
1199
|
+
continue
|
|
1200
|
+
formatted = ClipboardManager.format_results_as(last_result.columns, last_result.rows, format_type="csv")
|
|
1201
|
+
ok, msg = ClipboardManager.copy_text(formatted)
|
|
1202
|
+
if ok:
|
|
1203
|
+
print_success(f"Copied {len(last_result.rows)} row(s) as CSV to clipboard!")
|
|
1204
|
+
else:
|
|
1205
|
+
print_error(f"Clipboard copy failed: {msg}")
|
|
1206
|
+
elif target in ["tsv", "excel", "sheets", "tab"]:
|
|
1207
|
+
if not last_result or not last_result.rows:
|
|
1208
|
+
print_warning("No query result rows to copy.")
|
|
1209
|
+
continue
|
|
1210
|
+
formatted = ClipboardManager.format_results_as(last_result.columns, last_result.rows, format_type="tsv")
|
|
1211
|
+
ok, msg = ClipboardManager.copy_text(formatted)
|
|
1212
|
+
if ok:
|
|
1213
|
+
print_success(f"Copied {len(last_result.rows)} row(s) as TSV to clipboard (ready for Excel/Sheets)!")
|
|
1214
|
+
else:
|
|
1215
|
+
print_error(f"Clipboard copy failed: {msg}")
|
|
1216
|
+
elif target in ["json", "js"]:
|
|
1217
|
+
if not last_result or not last_result.rows:
|
|
1218
|
+
print_warning("No query result rows to copy.")
|
|
1219
|
+
continue
|
|
1220
|
+
formatted = ClipboardManager.format_results_as(last_result.columns, last_result.rows, format_type="json")
|
|
1221
|
+
ok, msg = ClipboardManager.copy_text(formatted)
|
|
1222
|
+
if ok:
|
|
1223
|
+
print_success(f"Copied {len(last_result.rows)} row(s) as JSON to clipboard!")
|
|
1224
|
+
else:
|
|
1225
|
+
print_error(f"Clipboard copy failed: {msg}")
|
|
1226
|
+
elif target in ["md", "markdown", "table"]:
|
|
1227
|
+
if not last_result or not last_result.rows:
|
|
1228
|
+
print_warning("No query result rows to copy.")
|
|
1229
|
+
continue
|
|
1230
|
+
formatted = ClipboardManager.format_results_as(last_result.columns, last_result.rows, format_type="md")
|
|
1231
|
+
ok, msg = ClipboardManager.copy_text(formatted)
|
|
1232
|
+
if ok:
|
|
1233
|
+
print_success(f"Copied {len(last_result.rows)} row(s) as Markdown table to clipboard!")
|
|
1234
|
+
else:
|
|
1235
|
+
print_error(f"Clipboard copy failed: {msg}")
|
|
1236
|
+
elif target in ["insert", "inserts", "sql-insert"]:
|
|
1237
|
+
if not last_result or not last_result.rows:
|
|
1238
|
+
print_warning("No query result rows to copy.")
|
|
1239
|
+
continue
|
|
1240
|
+
tbl = last_result.exact_tables[0] if (last_result.exact_tables) else "query_results"
|
|
1241
|
+
formatted = ClipboardManager.format_results_as(last_result.columns, last_result.rows, format_type="sql", table_name=tbl)
|
|
1242
|
+
ok, msg = ClipboardManager.copy_text(formatted)
|
|
1243
|
+
if ok:
|
|
1244
|
+
print_success(f"Copied {len(last_result.rows)} SQL INSERT statement(s) to clipboard!")
|
|
1245
|
+
else:
|
|
1246
|
+
print_error(f"Clipboard copy failed: {msg}")
|
|
1247
|
+
elif target in ["prompt", "question", "ask"]:
|
|
1248
|
+
if not last_user_prompt:
|
|
1249
|
+
print_warning("No previous prompt to copy.")
|
|
1250
|
+
continue
|
|
1251
|
+
ok, msg = ClipboardManager.copy_text(last_user_prompt)
|
|
1252
|
+
if ok:
|
|
1253
|
+
print_success("Copied last prompt to clipboard!")
|
|
1254
|
+
else:
|
|
1255
|
+
print_error(f"Clipboard copy failed: {msg}")
|
|
1256
|
+
elif target in ["history", "all-sql"]:
|
|
1257
|
+
sqls = [r.sql.strip() for r in query_history if r.sql]
|
|
1258
|
+
if not sqls:
|
|
1259
|
+
print_warning("No SQL queries in session history to copy.")
|
|
1260
|
+
continue
|
|
1261
|
+
joined = ";\n\n".join(sqls) + ";"
|
|
1262
|
+
ok, msg = ClipboardManager.copy_text(joined)
|
|
1263
|
+
if ok:
|
|
1264
|
+
print_success(f"Copied {len(sqls)} SQL statement(s) from session history to clipboard!")
|
|
1265
|
+
else:
|
|
1266
|
+
print_error(f"Clipboard copy failed: {msg}")
|
|
1267
|
+
else:
|
|
1268
|
+
print_warning(f"Unknown copy format '{target}'. Available: :copy sql, :copy csv, :copy tsv (Excel), :copy json, :copy md, :copy insert, :copy prompt, :copy history")
|
|
1269
|
+
continue
|
|
1270
|
+
|
|
1271
|
+
if cmd_name in [":paste", ":p"]:
|
|
1272
|
+
pasted_text, p_msg = ClipboardManager.get_text()
|
|
1273
|
+
if not pasted_text or not pasted_text.strip():
|
|
1274
|
+
print_warning("Clipboard is empty or could not be read.")
|
|
1275
|
+
continue
|
|
1276
|
+
pasted_clean = pasted_text.strip()
|
|
1277
|
+
console.print(f"\n[bold cyan]📋 Pasted from clipboard ({len(pasted_clean):,} chars):[/bold cyan]")
|
|
1278
|
+
if "\n" in pasted_clean or len(pasted_clean) > 80:
|
|
1279
|
+
if any(pasted_clean.lower().startswith(kw) for kw in ["select", "insert", "update", "delete", "create", "alter", "drop", "with", "explain"]):
|
|
1280
|
+
from rich.syntax import Syntax
|
|
1281
|
+
console.print(Panel(Syntax(pasted_clean, "sql", theme="monokai", line_numbers=False), border_style="cyan", padding=(0, 1)))
|
|
1282
|
+
else:
|
|
1283
|
+
console.print(Panel(pasted_clean, border_style="cyan", padding=(0, 1)))
|
|
1284
|
+
else:
|
|
1285
|
+
console.print(f" [bold white]{pasted_clean}[/bold white]\n")
|
|
1286
|
+
|
|
1287
|
+
user_input = pasted_clean
|
|
1288
|
+
if user_input.startswith(":"):
|
|
1289
|
+
prompt_queue.split_and_enqueue_chained(user_input)
|
|
1290
|
+
continue
|
|
1291
|
+
|
|
1292
|
+
elif cmd_name in [":multiline", ":paste-mode", ":m"]:
|
|
1293
|
+
multiline_input = open_multiline_paste_session()
|
|
1294
|
+
if not multiline_input:
|
|
1295
|
+
continue
|
|
1296
|
+
user_input = multiline_input
|
|
1297
|
+
if user_input.startswith(":"):
|
|
1298
|
+
prompt_queue.split_and_enqueue_chained(user_input)
|
|
1299
|
+
continue
|
|
1300
|
+
|
|
1301
|
+
elif cmd_name in [":page", ":cols"]:
|
|
1128
1302
|
if not last_result or not last_result.rows:
|
|
1129
1303
|
print_warning("No previous query results to page.")
|
|
1130
1304
|
continue
|
|
@@ -1135,21 +1309,21 @@ def chat_command(
|
|
|
1135
1309
|
print_results_table(last_result.columns, last_result.rows, title="Results", col_page=p_num)
|
|
1136
1310
|
continue
|
|
1137
1311
|
|
|
1138
|
-
|
|
1312
|
+
elif cmd_name in [":card", ":vertical", ":x", ":v"]:
|
|
1139
1313
|
if not last_result or not last_result.rows:
|
|
1140
1314
|
print_warning("No previous query results to display.")
|
|
1141
1315
|
continue
|
|
1142
1316
|
print_results_vertical(last_result.columns, last_result.rows)
|
|
1143
1317
|
continue
|
|
1144
1318
|
|
|
1145
|
-
|
|
1319
|
+
elif cmd_name in [":wide", ":all", ":raw"]:
|
|
1146
1320
|
if not last_result or not last_result.rows:
|
|
1147
1321
|
print_warning("No previous query results to display.")
|
|
1148
1322
|
continue
|
|
1149
1323
|
print_results_table(last_result.columns, last_result.rows, title="All Columns (Wide View)", show_all=True)
|
|
1150
1324
|
continue
|
|
1151
1325
|
|
|
1152
|
-
|
|
1326
|
+
elif cmd_name in [":window", ":gui", ":grid", ":w"]:
|
|
1153
1327
|
if not last_result or not last_result.rows:
|
|
1154
1328
|
print_warning("No previous query results to display.")
|
|
1155
1329
|
continue
|
|
@@ -1157,7 +1331,7 @@ def chat_command(
|
|
|
1157
1331
|
open_desktop_grid_window(last_result.columns, last_result.rows, title=f"DB-Agent Data Grid")
|
|
1158
1332
|
continue
|
|
1159
1333
|
|
|
1160
|
-
|
|
1334
|
+
elif cmd_name in [":web", ":browser", ":html"]:
|
|
1161
1335
|
if not last_result or not last_result.rows:
|
|
1162
1336
|
print_warning("No previous query results to display.")
|
|
1163
1337
|
continue
|
|
@@ -1165,24 +1339,24 @@ def chat_command(
|
|
|
1165
1339
|
open_browser_grid_window(last_result.columns, last_result.rows, title=f"DB-Agent Data View")
|
|
1166
1340
|
continue
|
|
1167
1341
|
|
|
1168
|
-
|
|
1342
|
+
elif cmd_name in [":scroll", ":scroller", ":pager"]:
|
|
1169
1343
|
if not last_result or not last_result.rows:
|
|
1170
1344
|
print_warning("No previous query results to scroll.")
|
|
1171
1345
|
continue
|
|
1172
1346
|
open_interactive_scroller(last_result.columns, last_result.rows)
|
|
1173
1347
|
continue
|
|
1174
1348
|
|
|
1175
|
-
|
|
1349
|
+
elif cmd_name == ":auto":
|
|
1176
1350
|
auto_execute = True
|
|
1177
1351
|
print_success("Auto-execute enabled. Read queries will run automatically.")
|
|
1178
1352
|
continue
|
|
1179
1353
|
|
|
1180
|
-
|
|
1354
|
+
elif cmd_name == ":noauto":
|
|
1181
1355
|
auto_execute = False
|
|
1182
1356
|
print_info("Auto-execute disabled. All queries will show SQL only. Use :run to execute.")
|
|
1183
1357
|
continue
|
|
1184
1358
|
|
|
1185
|
-
|
|
1359
|
+
elif cmd_name == ":run":
|
|
1186
1360
|
if not last_result or not last_result.sql:
|
|
1187
1361
|
print_warning("No generated SQL query to run.")
|
|
1188
1362
|
continue
|
|
@@ -1195,7 +1369,7 @@ def chat_command(
|
|
|
1195
1369
|
print_results_table(cols, rows)
|
|
1196
1370
|
continue
|
|
1197
1371
|
|
|
1198
|
-
|
|
1372
|
+
elif cmd_name == ":export":
|
|
1199
1373
|
if not cmd_arg:
|
|
1200
1374
|
print_warning("Usage: :export <file_path> (e.g. :export query.sql)")
|
|
1201
1375
|
continue
|
|
@@ -1208,13 +1382,16 @@ def chat_command(
|
|
|
1208
1382
|
print_success(f"SQL saved to {p}")
|
|
1209
1383
|
continue
|
|
1210
1384
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1385
|
+
else:
|
|
1386
|
+
print_warning(f"Unknown command '{user_input}'. Type [bold]:help[/bold] to see all in-chat commands.")
|
|
1387
|
+
continue
|
|
1213
1388
|
|
|
1214
1389
|
if user_input.lower() in ["exit", "quit", "q"]:
|
|
1215
1390
|
console.print("[bold cyan]Goodbye![/bold cyan]")
|
|
1216
1391
|
break
|
|
1217
1392
|
|
|
1393
|
+
last_user_prompt = user_input
|
|
1394
|
+
|
|
1218
1395
|
# --- Natural language query via pipeline with live streaming & background queue listener ---
|
|
1219
1396
|
pipeline.model = model
|
|
1220
1397
|
pipeline.mode = current_mode
|
|
@@ -60,12 +60,13 @@ def render_chat_header(
|
|
|
60
60
|
right_grid.add_column()
|
|
61
61
|
right_grid.add_row("[bold cyan]💡 Quick Database Tips[/bold cyan]")
|
|
62
62
|
right_grid.add_row("[dim]Ask in plain English or paste direct SQL for 0ms execution[/dim]")
|
|
63
|
+
right_grid.add_row("[dim]Copy results: [bold white]:copy[/bold white] (SQL), [bold white]:copy csv[/bold white], [bold white]:copy tsv[/bold white] (Excel) · Paste: [bold white]:paste[/bold white][/dim]")
|
|
63
64
|
right_grid.add_row("[dim]Type :help to inspect tables (:tables), relations (:fk), or diagnostics (:doctor)[/dim]")
|
|
64
65
|
right_grid.add_row("[dim]────────────────────────────────────────────────────────────────────────[/dim]")
|
|
65
66
|
right_grid.add_row(f"[bold green]🚀 Features in v{__version__}[/bold green]")
|
|
66
67
|
right_grid.add_row("[dim]Universal AI: Claude 3.7 Sonnet, OpenAI GPT-4o, DeepSeek R1, Mistral, Gemini[/dim]")
|
|
67
68
|
right_grid.add_row("[dim]Zero-Latency Direct SQL Fast-Path with autonomous AI auto-repair on error[/dim]")
|
|
68
|
-
right_grid.add_row("[dim]
|
|
69
|
+
right_grid.add_row("[dim]Cross-platform instant clipboard copy (SQL, CSV, TSV Excel, JSON) & bracketed paste[/dim]")
|
|
69
70
|
|
|
70
71
|
layout_table = Table.grid(expand=True, padding=(0, 2))
|
|
71
72
|
layout_table.add_column(ratio=2)
|
|
@@ -81,7 +82,7 @@ def render_chat_header(
|
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
console.print(header_panel)
|
|
84
|
-
console.print(f"[bold cyan]│[/bold cyan] [bold cyan]⚡ Multi-AI Engine Live![/bold cyan] [dim]Type [bold white]:models[/bold white] to switch AI models · [bold white]:
|
|
85
|
+
console.print(f"[bold cyan]│[/bold cyan] [bold cyan]⚡ Multi-AI Engine Live![/bold cyan] [dim]Type [bold white]:models[/bold white] to switch AI models · [bold white]:copy[/bold white] to copy · Mode: [bold yellow]{mode}[/bold yellow][/dim]\n")
|
|
85
86
|
|
|
86
87
|
if recommendations:
|
|
87
88
|
console.print("[dim italic]💡 Try asking:[/dim italic]")
|
|
@@ -177,8 +178,12 @@ def render_assistant_turn(
|
|
|
177
178
|
)
|
|
178
179
|
console.print(bar_chart)
|
|
179
180
|
console.print("")
|
|
181
|
+
|
|
182
|
+
console.print("[dim]💡 Quick Copy: [bold white]:copy[/bold white] (SQL) · [bold white]:copy csv[/bold white] / [bold white]:copy tsv[/bold white] (Excel) / [bold white]:copy json[/bold white] · [bold white]:paste[/bold white] to run from clipboard[/dim]")
|
|
180
183
|
else:
|
|
181
184
|
print_info("Query executed successfully. No rows returned.")
|
|
185
|
+
if result.sql:
|
|
186
|
+
console.print("[dim]💡 Quick Copy: [bold white]:copy[/bold white] (SQL) · [bold white]:paste[/bold white] to run from clipboard[/dim]")
|
|
182
187
|
|
|
183
188
|
if result.confidence:
|
|
184
189
|
console.print(result.confidence.format_summary())
|
|
@@ -204,7 +209,7 @@ def render_assistant_turn(
|
|
|
204
209
|
elif result.sql:
|
|
205
210
|
syntax = Syntax(result.sql.strip(), "sql", theme="monokai", line_numbers=False)
|
|
206
211
|
console.print(Panel(syntax, title="Generated SQL", border_style="dim"))
|
|
207
|
-
console.print("")
|
|
212
|
+
console.print("[dim]💡 Quick Copy: [bold white]:copy[/bold white] (SQL) · [bold white]:paste[/bold white] to run from clipboard[/dim]\n")
|
|
208
213
|
|
|
209
214
|
|
|
210
215
|
def render_session_history(query_history: List[PipelineResult]) -> None:
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-Platform Clipboard & Multiline Paste Engine for DB-Agent.
|
|
3
|
+
Provides zero-dependency, ultra-fast system clipboard access across Windows, macOS, and Linux,
|
|
4
|
+
with formatting support for SQL, CSV, TSV, JSON, and Markdown tables.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import csv
|
|
10
|
+
import json
|
|
11
|
+
import io
|
|
12
|
+
import subprocess
|
|
13
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClipboardManager:
|
|
17
|
+
"""
|
|
18
|
+
Zero-dependency, cross-platform clipboard manager.
|
|
19
|
+
Supports Windows (Win32 ctypes + PowerShell), macOS (pbcopy/pbpaste), and Linux (wl-copy, xclip, xsel).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def copy_text(cls, text: str) -> Tuple[bool, str]:
|
|
24
|
+
"""
|
|
25
|
+
Copy text to the OS system clipboard.
|
|
26
|
+
Returns (success, message).
|
|
27
|
+
"""
|
|
28
|
+
if not text:
|
|
29
|
+
return False, "Nothing to copy (empty text)."
|
|
30
|
+
|
|
31
|
+
# 1. Try pyperclip if installed
|
|
32
|
+
try:
|
|
33
|
+
import pyperclip
|
|
34
|
+
pyperclip.copy(text)
|
|
35
|
+
return True, f"Copied {len(text):,} characters to clipboard"
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# 2. Windows: Native Win32 API via ctypes (zero-latency <0.1ms)
|
|
40
|
+
if sys.platform == "win32":
|
|
41
|
+
try:
|
|
42
|
+
import ctypes
|
|
43
|
+
from ctypes import wintypes
|
|
44
|
+
|
|
45
|
+
CF_UNICODETEXT = 13
|
|
46
|
+
GMEM_MOVEABLE = 0x0002
|
|
47
|
+
|
|
48
|
+
user32 = ctypes.windll.user32
|
|
49
|
+
kernel32 = ctypes.windll.kernel32
|
|
50
|
+
|
|
51
|
+
kernel32.GlobalAlloc.restype = ctypes.c_void_p
|
|
52
|
+
kernel32.GlobalAlloc.argtypes = [wintypes.UINT, ctypes.c_size_t]
|
|
53
|
+
kernel32.GlobalLock.restype = ctypes.c_void_p
|
|
54
|
+
kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
|
|
55
|
+
kernel32.GlobalUnlock.restype = wintypes.BOOL
|
|
56
|
+
kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
|
|
57
|
+
|
|
58
|
+
user32.OpenClipboard.restype = wintypes.BOOL
|
|
59
|
+
user32.OpenClipboard.argtypes = [wintypes.HWND]
|
|
60
|
+
user32.EmptyClipboard.restype = wintypes.BOOL
|
|
61
|
+
user32.CloseClipboard.restype = wintypes.BOOL
|
|
62
|
+
user32.SetClipboardData.restype = ctypes.c_void_p
|
|
63
|
+
user32.SetClipboardData.argtypes = [wintypes.UINT, ctypes.c_void_p]
|
|
64
|
+
|
|
65
|
+
if user32.OpenClipboard(None):
|
|
66
|
+
try:
|
|
67
|
+
user32.EmptyClipboard()
|
|
68
|
+
encoded = text.encode("utf-16le") + b"\x00\x00"
|
|
69
|
+
h_mem = kernel32.GlobalAlloc(GMEM_MOVEABLE, len(encoded))
|
|
70
|
+
if h_mem:
|
|
71
|
+
p_mem = kernel32.GlobalLock(h_mem)
|
|
72
|
+
if p_mem:
|
|
73
|
+
ctypes.memmove(p_mem, encoded, len(encoded))
|
|
74
|
+
kernel32.GlobalUnlock(h_mem)
|
|
75
|
+
user32.SetClipboardData(CF_UNICODETEXT, h_mem)
|
|
76
|
+
return True, f"Copied {len(text):,} characters to clipboard"
|
|
77
|
+
finally:
|
|
78
|
+
user32.CloseClipboard()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Windows Fallback: PowerShell Set-Clipboard
|
|
83
|
+
try:
|
|
84
|
+
proc = subprocess.Popen(
|
|
85
|
+
["powershell", "-NoProfile", "-Command", "Set-Clipboard -Value $input"],
|
|
86
|
+
stdin=subprocess.PIPE,
|
|
87
|
+
stdout=subprocess.PIPE,
|
|
88
|
+
stderr=subprocess.PIPE,
|
|
89
|
+
text=True,
|
|
90
|
+
)
|
|
91
|
+
proc.communicate(input=text, timeout=3)
|
|
92
|
+
if proc.returncode == 0:
|
|
93
|
+
return True, f"Copied {len(text):,} characters to clipboard"
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
# 3. macOS: pbcopy
|
|
98
|
+
elif sys.platform == "darwin":
|
|
99
|
+
try:
|
|
100
|
+
proc = subprocess.Popen(
|
|
101
|
+
["pbcopy"],
|
|
102
|
+
stdin=subprocess.PIPE,
|
|
103
|
+
stdout=subprocess.PIPE,
|
|
104
|
+
stderr=subprocess.PIPE,
|
|
105
|
+
)
|
|
106
|
+
proc.communicate(input=text.encode("utf-8"), timeout=3)
|
|
107
|
+
if proc.returncode == 0:
|
|
108
|
+
return True, f"Copied {len(text):,} characters to clipboard"
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# 4. Linux: Wayland wl-copy or X11 xclip/xsel
|
|
113
|
+
else:
|
|
114
|
+
for cmd in [["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]]:
|
|
115
|
+
try:
|
|
116
|
+
proc = subprocess.Popen(
|
|
117
|
+
cmd,
|
|
118
|
+
stdin=subprocess.PIPE,
|
|
119
|
+
stdout=subprocess.PIPE,
|
|
120
|
+
stderr=subprocess.PIPE,
|
|
121
|
+
)
|
|
122
|
+
proc.communicate(input=text.encode("utf-8"), timeout=3)
|
|
123
|
+
if proc.returncode == 0:
|
|
124
|
+
return True, f"Copied {len(text):,} characters to clipboard"
|
|
125
|
+
except Exception:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
return False, "Could not access system clipboard."
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def get_text(cls) -> Tuple[Optional[str], str]:
|
|
132
|
+
"""
|
|
133
|
+
Get text from the OS system clipboard.
|
|
134
|
+
Returns (text, message).
|
|
135
|
+
"""
|
|
136
|
+
# 1. Try pyperclip if installed
|
|
137
|
+
try:
|
|
138
|
+
import pyperclip
|
|
139
|
+
val = pyperclip.paste()
|
|
140
|
+
if val:
|
|
141
|
+
return val, "Retrieved text from clipboard"
|
|
142
|
+
except ImportError:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
# 2. Windows: Native Win32 API via ctypes
|
|
146
|
+
if sys.platform == "win32":
|
|
147
|
+
try:
|
|
148
|
+
import ctypes
|
|
149
|
+
from ctypes import wintypes
|
|
150
|
+
|
|
151
|
+
CF_UNICODETEXT = 13
|
|
152
|
+
user32 = ctypes.windll.user32
|
|
153
|
+
kernel32 = ctypes.windll.kernel32
|
|
154
|
+
|
|
155
|
+
kernel32.GlobalLock.restype = ctypes.c_void_p
|
|
156
|
+
kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
|
|
157
|
+
kernel32.GlobalUnlock.restype = wintypes.BOOL
|
|
158
|
+
kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
|
|
159
|
+
|
|
160
|
+
user32.OpenClipboard.restype = wintypes.BOOL
|
|
161
|
+
user32.OpenClipboard.argtypes = [wintypes.HWND]
|
|
162
|
+
user32.CloseClipboard.restype = wintypes.BOOL
|
|
163
|
+
user32.GetClipboardData.restype = ctypes.c_void_p
|
|
164
|
+
user32.GetClipboardData.argtypes = [wintypes.UINT]
|
|
165
|
+
|
|
166
|
+
if user32.OpenClipboard(None):
|
|
167
|
+
try:
|
|
168
|
+
h_mem = user32.GetClipboardData(CF_UNICODETEXT)
|
|
169
|
+
if h_mem:
|
|
170
|
+
p_mem = kernel32.GlobalLock(h_mem)
|
|
171
|
+
if p_mem:
|
|
172
|
+
text = ctypes.c_wchar_p(p_mem).value
|
|
173
|
+
kernel32.GlobalUnlock(h_mem)
|
|
174
|
+
if text:
|
|
175
|
+
return text, f"Pasted {len(text):,} characters from clipboard"
|
|
176
|
+
finally:
|
|
177
|
+
user32.CloseClipboard()
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
# Windows Fallback: PowerShell Get-Clipboard
|
|
182
|
+
try:
|
|
183
|
+
output = subprocess.check_output(
|
|
184
|
+
["powershell", "-NoProfile", "-Command", "Get-Clipboard"],
|
|
185
|
+
text=True,
|
|
186
|
+
timeout=3,
|
|
187
|
+
)
|
|
188
|
+
if output:
|
|
189
|
+
return output.strip(), f"Pasted {len(output):,} characters from clipboard"
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
# 3. macOS: pbpaste
|
|
194
|
+
elif sys.platform == "darwin":
|
|
195
|
+
try:
|
|
196
|
+
output = subprocess.check_output(["pbpaste"], text=True, timeout=3)
|
|
197
|
+
if output:
|
|
198
|
+
return output.strip(), f"Pasted {len(output):,} characters from clipboard"
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
# 4. Linux: Wayland wl-paste or X11 xclip/xsel
|
|
203
|
+
else:
|
|
204
|
+
for cmd in [["wl-paste"], ["xclip", "-selection", "clipboard", "-o"], ["xsel", "--clipboard", "--output"]]:
|
|
205
|
+
try:
|
|
206
|
+
output = subprocess.check_output(cmd, text=True, timeout=3)
|
|
207
|
+
if output:
|
|
208
|
+
return output.strip(), f"Pasted {len(output):,} characters from clipboard"
|
|
209
|
+
except Exception:
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
return None, "Clipboard is empty or unavailable."
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def format_results_as(
|
|
216
|
+
cls,
|
|
217
|
+
columns: List[str],
|
|
218
|
+
rows: List[Dict[str, Any]],
|
|
219
|
+
format_type: str = "csv",
|
|
220
|
+
table_name: str = "query_results",
|
|
221
|
+
) -> str:
|
|
222
|
+
"""
|
|
223
|
+
Formats query result set into CSV, TSV, JSON, Markdown, or SQL INSERT statements.
|
|
224
|
+
"""
|
|
225
|
+
fmt = format_type.strip().lower()
|
|
226
|
+
|
|
227
|
+
# CSV format
|
|
228
|
+
if fmt == "csv":
|
|
229
|
+
out = io.StringIO()
|
|
230
|
+
writer = csv.DictWriter(out, fieldnames=columns)
|
|
231
|
+
writer.writeheader()
|
|
232
|
+
for r in rows:
|
|
233
|
+
writer.writerow(r)
|
|
234
|
+
return out.getvalue().strip()
|
|
235
|
+
|
|
236
|
+
# TSV format (Tab-separated, directly pasteable into Excel / Google Sheets)
|
|
237
|
+
if fmt in ("tsv", "excel", "sheets"):
|
|
238
|
+
out = io.StringIO()
|
|
239
|
+
writer = csv.DictWriter(out, fieldnames=columns, delimiter="\t")
|
|
240
|
+
writer.writeheader()
|
|
241
|
+
for r in rows:
|
|
242
|
+
writer.writerow(r)
|
|
243
|
+
return out.getvalue().strip()
|
|
244
|
+
|
|
245
|
+
# JSON format
|
|
246
|
+
if fmt in ("json", "js"):
|
|
247
|
+
return json.dumps(rows, indent=2, default=str)
|
|
248
|
+
|
|
249
|
+
# Markdown Table format
|
|
250
|
+
if fmt in ("md", "markdown"):
|
|
251
|
+
lines = []
|
|
252
|
+
lines.append("| " + " | ".join(columns) + " |")
|
|
253
|
+
lines.append("| " + " | ".join(["---"] * len(columns)) + " |")
|
|
254
|
+
for r in rows:
|
|
255
|
+
row_vals = [str(r.get(c, "")).replace("\n", " ").replace("|", "\\|") for c in columns]
|
|
256
|
+
lines.append("| " + " | ".join(row_vals) + " |")
|
|
257
|
+
return "\n".join(lines)
|
|
258
|
+
|
|
259
|
+
# SQL INSERT statements format
|
|
260
|
+
if fmt in ("sql", "insert", "inserts"):
|
|
261
|
+
stmts = []
|
|
262
|
+
cols_str = ", ".join(f"`{c}`" for c in columns)
|
|
263
|
+
for r in rows:
|
|
264
|
+
val_parts = []
|
|
265
|
+
for c in columns:
|
|
266
|
+
v = r.get(c)
|
|
267
|
+
if v is None:
|
|
268
|
+
val_parts.append("NULL")
|
|
269
|
+
elif isinstance(v, bool):
|
|
270
|
+
val_parts.append("TRUE" if v else "FALSE")
|
|
271
|
+
elif isinstance(v, (int, float)):
|
|
272
|
+
val_parts.append(str(v))
|
|
273
|
+
else:
|
|
274
|
+
escaped = str(v).replace("'", "''")
|
|
275
|
+
val_parts.append(f"'{escaped}'")
|
|
276
|
+
stmts.append(f"INSERT INTO {table_name} ({cols_str}) VALUES ({', '.join(val_parts)});")
|
|
277
|
+
return "\n".join(stmts)
|
|
278
|
+
|
|
279
|
+
# Default fallback to CSV
|
|
280
|
+
return cls.format_results_as(columns, rows, format_type="csv", table_name=table_name)
|
|
@@ -60,6 +60,7 @@ dbagent/sql/__init__.py
|
|
|
60
60
|
dbagent/sql/optimizer.py
|
|
61
61
|
dbagent/sql/validator.py
|
|
62
62
|
dbagent/ui/chat_screen.py
|
|
63
|
+
dbagent/ui/clipboard.py
|
|
63
64
|
dbagent/ui/console.py
|
|
64
65
|
dbagent/ui/grid_window.py
|
|
65
66
|
dbagent/ui/live_streamer.py
|
|
@@ -74,6 +75,7 @@ dbagent_cli.egg-info/top_level.txt
|
|
|
74
75
|
tests/test_benchmarks.py
|
|
75
76
|
tests/test_cache.py
|
|
76
77
|
tests/test_cli.py
|
|
78
|
+
tests/test_clipboard.py
|
|
77
79
|
tests/test_connectors.py
|
|
78
80
|
tests/test_demo_and_sample_db.py
|
|
79
81
|
tests/test_direct_sql.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbagent-cli"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.5"
|
|
8
8
|
description = "The Privacy-First AI Database Copilot, Schema Graph Introspector & Post-Commit Recovery Engine (CLI)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for ClipboardManager cross-platform copy and paste engine.
|
|
3
|
+
Verifies formatters (CSV, TSV, JSON, Markdown, SQL INSERT) and clipboard operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import pytest
|
|
8
|
+
from dbagent.ui.clipboard import ClipboardManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def sample_data():
|
|
13
|
+
columns = ["id", "username", "email", "active", "balance", "notes"]
|
|
14
|
+
rows = [
|
|
15
|
+
{
|
|
16
|
+
"id": 1,
|
|
17
|
+
"username": "santhosh",
|
|
18
|
+
"email": "santhosh@example.com",
|
|
19
|
+
"active": True,
|
|
20
|
+
"balance": 150.50,
|
|
21
|
+
"notes": "Admin user",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": 2,
|
|
25
|
+
"username": "alice",
|
|
26
|
+
"email": "alice@example.com",
|
|
27
|
+
"active": False,
|
|
28
|
+
"balance": 0.0,
|
|
29
|
+
"notes": "Test account",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": 3,
|
|
33
|
+
"username": "bob",
|
|
34
|
+
"email": "bob@example.com",
|
|
35
|
+
"active": True,
|
|
36
|
+
"balance": None,
|
|
37
|
+
"notes": None,
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
return columns, rows
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_format_results_csv(sample_data):
|
|
44
|
+
columns, rows = sample_data
|
|
45
|
+
csv_out = ClipboardManager.format_results_as(columns, rows, format_type="csv")
|
|
46
|
+
lines = csv_out.splitlines()
|
|
47
|
+
|
|
48
|
+
assert len(lines) == 4
|
|
49
|
+
assert lines[0] == "id,username,email,active,balance,notes"
|
|
50
|
+
assert "santhosh@example.com" in lines[1]
|
|
51
|
+
assert "alice@example.com" in lines[2]
|
|
52
|
+
assert "bob@example.com" in lines[3]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_format_results_tsv(sample_data):
|
|
56
|
+
columns, rows = sample_data
|
|
57
|
+
tsv_out = ClipboardManager.format_results_as(columns, rows, format_type="tsv")
|
|
58
|
+
lines = tsv_out.splitlines()
|
|
59
|
+
|
|
60
|
+
assert len(lines) == 4
|
|
61
|
+
assert lines[0] == "id\tusername\temail\tactive\tbalance\tnotes"
|
|
62
|
+
assert "santhosh\tsanthosh@example.com" in lines[1]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_format_results_excel_alias(sample_data):
|
|
66
|
+
columns, rows = sample_data
|
|
67
|
+
excel_out = ClipboardManager.format_results_as(columns, rows, format_type="excel")
|
|
68
|
+
sheets_out = ClipboardManager.format_results_as(columns, rows, format_type="sheets")
|
|
69
|
+
|
|
70
|
+
assert "\t" in excel_out
|
|
71
|
+
assert excel_out == sheets_out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_format_results_json(sample_data):
|
|
75
|
+
columns, rows = sample_data
|
|
76
|
+
json_out = ClipboardManager.format_results_as(columns, rows, format_type="json")
|
|
77
|
+
parsed = json.loads(json_out)
|
|
78
|
+
|
|
79
|
+
assert len(parsed) == 3
|
|
80
|
+
assert parsed[0]['username'] == "santhosh"
|
|
81
|
+
assert parsed[0]['active'] is True
|
|
82
|
+
assert parsed[1]['username'] == "alice"
|
|
83
|
+
assert parsed[2]['balance'] is None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_format_results_markdown(sample_data):
|
|
87
|
+
columns, rows = sample_data
|
|
88
|
+
md_out = ClipboardManager.format_results_as(columns, rows, format_type="md")
|
|
89
|
+
lines = md_out.splitlines()
|
|
90
|
+
|
|
91
|
+
assert len(lines) == 5
|
|
92
|
+
assert "id" in lines[0] and "username" in lines[0]
|
|
93
|
+
assert "santhosh" in lines[2]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_format_results_sql_insert(sample_data):
|
|
97
|
+
columns, rows = sample_data
|
|
98
|
+
sql_out = ClipboardManager.format_results_as(columns, rows, format_type="sql", table_name="users")
|
|
99
|
+
stmts = sql_out.splitlines()
|
|
100
|
+
|
|
101
|
+
assert len(stmts) == 3
|
|
102
|
+
assert "INSERT INTO users" in stmts[0]
|
|
103
|
+
assert "TRUE" in stmts[0]
|
|
104
|
+
assert "FALSE" in stmts[1]
|
|
105
|
+
assert "NULL" in stmts[2]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_copy_text_empty():
|
|
109
|
+
success, msg = ClipboardManager.copy_text("")
|
|
110
|
+
assert not success
|
|
111
|
+
assert "Nothing to copy" in msg
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_clipboard_roundtrip_if_supported():
|
|
115
|
+
test_str = "DB-Agent Clipboard Test String 2026"
|
|
116
|
+
success, msg = ClipboardManager.copy_text(test_str)
|
|
117
|
+
if success:
|
|
118
|
+
retrieved, get_msg = ClipboardManager.get_text()
|
|
119
|
+
if retrieved is not None:
|
|
120
|
+
assert test_str in retrieved
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|