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.
Files changed (100) hide show
  1. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/PKG-INFO +1 -1
  2. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/__init__.py +1 -1
  3. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/cli.py +189 -12
  4. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/chat_screen.py +8 -3
  5. dbagent_cli-0.11.5/dbagent/ui/clipboard.py +280 -0
  6. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/PKG-INFO +1 -1
  7. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/SOURCES.txt +2 -0
  8. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/pyproject.toml +1 -1
  9. dbagent_cli-0.11.5/tests/test_clipboard.py +120 -0
  10. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/README.md +0 -0
  11. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/confidence.py +0 -0
  12. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/conversation.py +0 -0
  13. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/doctor.py +0 -0
  14. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/error_classifier.py +0 -0
  15. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/generator.py +0 -0
  16. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/pipeline.py +0 -0
  17. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/recommender.py +0 -0
  18. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/agent/validator.py +0 -0
  19. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/benchmark.py +0 -0
  20. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/config.py +0 -0
  21. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/connectors/base.py +0 -0
  22. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/connectors/factory.py +0 -0
  23. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/connectors/mongo.py +0 -0
  24. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/connectors/relational.py +0 -0
  25. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/errors.py +0 -0
  26. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/__init__.py +0 -0
  27. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/cache.py +0 -0
  28. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/changeset.py +0 -0
  29. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/cost.py +0 -0
  30. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/prompt_queue.py +0 -0
  31. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/recovery.py +0 -0
  32. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/sandbox.py +0 -0
  33. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/execution/streaming.py +0 -0
  34. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/anthropic_provider.py +0 -0
  35. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/auto_setup.py +0 -0
  36. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/base.py +0 -0
  37. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/deepseek_provider.py +0 -0
  38. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/factory.py +0 -0
  39. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/gemini_provider.py +0 -0
  40. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/groq_provider.py +0 -0
  41. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/mistral_provider.py +0 -0
  42. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/mock_provider.py +0 -0
  43. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/ollama_provider.py +0 -0
  44. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/openai_provider.py +0 -0
  45. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/llm/openrouter_provider.py +0 -0
  46. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/plugins.py +0 -0
  47. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/sample_db.py +0 -0
  48. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/cache.py +0 -0
  49. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/formatter.py +0 -0
  50. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/graph.py +0 -0
  51. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/ground_truth.py +0 -0
  52. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/models.py +0 -0
  53. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/retriever.py +0 -0
  54. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/schema/selector.py +0 -0
  55. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/__init__.py +0 -0
  56. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/audit.py +0 -0
  57. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/environment.py +0 -0
  58. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/masking.py +0 -0
  59. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/modes.py +0 -0
  60. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/security/rbac.py +0 -0
  61. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/semantic/__init__.py +0 -0
  62. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/semantic/dictionary.py +0 -0
  63. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/semantic/knowledge.py +0 -0
  64. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/semantic/templates.py +0 -0
  65. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/sql/__init__.py +0 -0
  66. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/sql/optimizer.py +0 -0
  67. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/sql/validator.py +0 -0
  68. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/console.py +0 -0
  69. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/grid_window.py +0 -0
  70. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/live_streamer.py +0 -0
  71. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/viewer.py +0 -0
  72. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent/ui/visualizer.py +0 -0
  73. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/dependency_links.txt +0 -0
  74. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/entry_points.txt +0 -0
  75. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/requires.txt +0 -0
  76. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/dbagent_cli.egg-info/top_level.txt +0 -0
  77. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/setup.cfg +0 -0
  78. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_benchmarks.py +0 -0
  79. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_cache.py +0 -0
  80. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_cli.py +0 -0
  81. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_connectors.py +0 -0
  82. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_demo_and_sample_db.py +0 -0
  83. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_direct_sql.py +0 -0
  84. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_doctor_and_viz.py +0 -0
  85. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_formats_and_exit_codes.py +0 -0
  86. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_generator.py +0 -0
  87. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_graph.py +0 -0
  88. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_ground_truth_sql_validation.py +0 -0
  89. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_knowledge.py +0 -0
  90. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_multi_turn_conversation.py +0 -0
  91. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_plugins.py +0 -0
  92. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_prompt_queue.py +0 -0
  93. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_prompt_queue_and_sql.py +0 -0
  94. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_providers.py +0 -0
  95. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_recovery.py +0 -0
  96. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_safety.py +0 -0
  97. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_sandbox.py +0 -0
  98. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_schema.py +0 -0
  99. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_security.py +0 -0
  100. {dbagent_cli-0.11.4 → dbagent_cli-0.11.5}/tests/test_semantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.11.4
3
+ Version: 0.11.5
4
4
  Summary: The Privacy-First AI Database Copilot, Schema Graph Introspector & Post-Commit Recovery Engine (CLI)
5
5
  Author: Santhosh Gupta
6
6
  License: MIT
@@ -2,5 +2,5 @@
2
2
  DB-Agent: Universal Database Introspection and Script Generation AI Agent (CLI).
3
3
  """
4
4
 
5
- __version__ = "0.11.4"
5
+ __version__ = "0.11.5"
6
6
  __author__ = "Santhosh Gupta"
@@ -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 [":page", ":cols", ":p"]:
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
- if cmd_name in [":card", ":vertical", ":x", ":v"]:
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
- if cmd_name in [":wide", ":all", ":raw"]:
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
- if cmd_name in [":window", ":gui", ":grid", ":w"]:
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
- if cmd_name in [":web", ":browser", ":html"]:
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
- if cmd_name in [":scroll", ":scroller", ":pager"]:
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
- if cmd_name == ":auto":
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
- if cmd_name == ":noauto":
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
- if cmd_name == ":run":
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
- if cmd_name == ":export":
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
- print_warning(f"Unknown command '{user_input}'. Type [bold]:help[/bold] to see all in-chat commands.")
1212
- continue
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]Multi-turn conversational session memory graph & persistent connection pooling[/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]:doctor[/bold white] for diagnostics · Mode: [bold yellow]{mode}[/bold yellow][/dim]\n")
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbagent-cli
3
- Version: 0.11.4
3
+ Version: 0.11.5
4
4
  Summary: The Privacy-First AI Database Copilot, Schema Graph Introspector & Post-Commit Recovery Engine (CLI)
5
5
  Author: Santhosh Gupta
6
6
  License: MIT
@@ -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.4"
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