cite-agent 1.0.5__py3-none-any.whl → 1.2.3__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.

Potentially problematic release.


This version of cite-agent might be problematic. Click here for more details.

cite_agent/cli.py CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Nocturnal Archive CLI - Command Line Interface
3
+ Cite Agent CLI - Command Line Interface
4
4
  Provides a terminal interface similar to cursor-agent
5
5
  """
6
6
 
@@ -24,14 +24,19 @@ from .enhanced_ai_agent import EnhancedNocturnalAgent, ChatRequest
24
24
  from .setup_config import NocturnalConfig, DEFAULT_QUERY_LIMIT, MANAGED_SECRETS
25
25
  from .telemetry import TelemetryManager
26
26
  from .updater import NocturnalUpdater
27
+ from .cli_workflow import WorkflowCLI
28
+ from .workflow import WorkflowManager, Paper, parse_paper_from_response
29
+ from .session_manager import SessionManager
27
30
 
28
31
  class NocturnalCLI:
29
- """Command Line Interface for Nocturnal Archive"""
32
+ """Command Line Interface for Cite Agent"""
30
33
 
31
34
  def __init__(self):
32
35
  self.agent: Optional[EnhancedNocturnalAgent] = None
33
36
  self.session_id = f"cli_{os.getpid()}"
34
37
  self.telemetry = None
38
+ self.workflow = WorkflowManager()
39
+ self.workflow_cli = WorkflowCLI()
35
40
  self.console = Console(theme=Theme({
36
41
  "banner": "bold magenta",
37
42
  "success": "bold green",
@@ -49,10 +54,32 @@ class NocturnalCLI:
49
54
  "If you see an auto-update notice, the CLI will restart itself to load the latest build.",
50
55
  ]
51
56
 
52
- async def initialize(self):
57
+ def handle_user_friendly_session(self):
58
+ """Handle session management with user-friendly interface"""
59
+ session_manager = SessionManager()
60
+
61
+ # Set up environment variables for backend mode
62
+ session_manager.setup_environment_variables()
63
+
64
+ # Handle session affirmation
65
+ result = session_manager.handle_session_affirmation()
66
+
67
+ if result == "error":
68
+ self.console.print("[red]❌ Session management failed. Please try again.[/red]")
69
+ return False
70
+
71
+ return True
72
+
73
+ async def initialize(self, non_interactive: bool = False):
53
74
  """Initialize the agent with automatic updates"""
54
75
  # Check for update notifications from previous runs
55
76
  self._check_update_notification()
77
+
78
+ # Handle user-friendly session management (skip prompts in non-interactive mode)
79
+ if not non_interactive:
80
+ if not self.handle_user_friendly_session():
81
+ return False
82
+
56
83
  self._show_intro_panel()
57
84
 
58
85
  self._enforce_latest_build()
@@ -74,6 +101,10 @@ class NocturnalCLI:
74
101
  self.console.print("[success]⚙️ Using saved credentials.[/success]")
75
102
  else:
76
103
  # Need interactive setup
104
+ if non_interactive:
105
+ self.console.print("[error]❌ Not authenticated. Run 'cite-agent --setup' to configure.[/error]")
106
+ return False
107
+
77
108
  self.console.print("\n[warning]👋 Hey there, looks like this machine hasn't met Nocturnal yet.[/warning]")
78
109
  self.console.print("[banner]Let's get you signed in — this only takes a minute.[/banner]")
79
110
  try:
@@ -101,8 +132,10 @@ class NocturnalCLI:
101
132
  self.console.print(" • Check your internet connection to the backend")
102
133
  return False
103
134
 
104
- self._show_ready_panel()
105
- self._show_beta_banner()
135
+ # Only show panels in debug mode or interactive mode
136
+ if not non_interactive or os.getenv("NOCTURNAL_DEBUG", "").lower() == "1":
137
+ self._show_ready_panel()
138
+ # Beta banner removed for production
106
139
  return True
107
140
 
108
141
  def _show_beta_banner(self):
@@ -131,13 +164,18 @@ class NocturnalCLI:
131
164
  self.console.print(panel)
132
165
 
133
166
  def _show_intro_panel(self):
167
+ # Only show in debug mode or interactive mode
168
+ debug_mode = os.getenv("NOCTURNAL_DEBUG", "").lower() == "1"
169
+ if not debug_mode:
170
+ return
171
+
134
172
  message = (
135
173
  "Warming up your research cockpit…\n"
136
174
  "[dim]Loading config, telemetry, and background update checks.[/dim]"
137
175
  )
138
176
  panel = Panel(
139
177
  message,
140
- title="🌙 Initializing Nocturnal Archive",
178
+ title="🤖 Initializing Cite Agent",
141
179
  border_style="magenta",
142
180
  padding=(1, 2),
143
181
  box=box.ROUNDED,
@@ -148,7 +186,7 @@ class NocturnalCLI:
148
186
  panel = Panel(
149
187
  "Systems check complete.\n"
150
188
  "Type [bold]help[/] for commands or [bold]tips[/] for power moves.",
151
- title="✅ Nocturnal Archive ready!",
189
+ title="✅ Cite Agent ready!",
152
190
  border_style="green",
153
191
  padding=(1, 2),
154
192
  box=box.ROUNDED,
@@ -230,27 +268,56 @@ class NocturnalCLI:
230
268
  if user_input.lower() == 'feedback':
231
269
  self.collect_feedback()
232
270
  continue
233
-
271
+
272
+ # Handle workflow commands
273
+ if user_input.lower() in ['show my library', 'library', 'list library']:
274
+ self.list_library()
275
+ continue
276
+ if user_input.lower() in ['show history', 'history']:
277
+ self.show_history()
278
+ continue
279
+ if user_input.lower().startswith('export bibtex'):
280
+ self.export_library_bibtex()
281
+ continue
282
+ if user_input.lower().startswith('export markdown'):
283
+ self.export_library_markdown()
284
+ continue
285
+
234
286
  if not user_input:
235
287
  continue
236
288
  except (EOFError, KeyboardInterrupt):
237
289
  self.console.print("\n[warning]👋 Goodbye![/warning]")
238
290
  break
239
291
 
240
- self.console.print("[bold violet]🤖 Agent[/]: ", end="", highlight=False)
241
-
242
292
  try:
243
- request = ChatRequest(
244
- question=user_input,
245
- user_id="cli_user",
246
- conversation_id=self.session_id
247
- )
248
-
249
- response = await self.agent.process_request(request)
293
+ from rich.spinner import Spinner
294
+ from rich.live import Live
250
295
 
296
+ # Show loading indicator while processing
297
+ with Live(Spinner("dots", text="[dim]Thinking...[/dim]"), console=self.console, transient=True):
298
+ request = ChatRequest(
299
+ question=user_input,
300
+ user_id="cli_user",
301
+ conversation_id=self.session_id
302
+ )
303
+
304
+ response = await self.agent.process_request(request)
305
+
251
306
  # Print response with proper formatting
307
+ self.console.print("[bold violet]🤖 Agent[/]: ", end="", highlight=False)
252
308
  self.console.print(response.response)
253
-
309
+
310
+ # Save to history automatically
311
+ self.workflow.save_query_result(
312
+ query=user_input,
313
+ response=response.response,
314
+ metadata={
315
+ "tools_used": response.tools_used,
316
+ "tokens_used": response.tokens_used,
317
+ "confidence_score": response.confidence_score
318
+ }
319
+ )
320
+
254
321
  # Show usage stats occasionally
255
322
  if hasattr(self.agent, 'daily_token_usage') and self.agent.daily_token_usage > 0:
256
323
  stats = self.agent.get_usage_stats()
@@ -266,25 +333,26 @@ class NocturnalCLI:
266
333
 
267
334
  async def single_query(self, question: str):
268
335
  """Process a single query"""
269
- if not await self.initialize():
336
+ if not await self.initialize(non_interactive=True):
270
337
  return
271
338
 
272
339
  try:
273
- self.console.print(f"🤖 [bold]Processing[/]: {question}")
274
- self.console.rule(style="magenta")
340
+ from rich.spinner import Spinner
341
+ from rich.live import Live
275
342
 
276
- request = ChatRequest(
277
- question=question,
278
- user_id="cli_user",
279
- conversation_id=self.session_id
280
- )
281
-
282
- response = await self.agent.process_request(request)
343
+ # Show clean loading indicator
344
+ with Live(Spinner("dots", text=f"[cyan]{question}[/cyan]"), console=self.console, transient=True):
345
+ request = ChatRequest(
346
+ question=question,
347
+ user_id="cli_user",
348
+ conversation_id=self.session_id
349
+ )
350
+
351
+ response = await self.agent.process_request(request)
283
352
 
284
353
  self.console.print(f"\n📝 [bold]Response[/]:\n{response.response}")
285
354
 
286
- if response.tools_used:
287
- self.console.print(f"\n🔧 Tools used: {', '.join(response.tools_used)}")
355
+ # Tools used removed for cleaner output
288
356
 
289
357
  if response.tokens_used > 0:
290
358
  stats = self.agent.get_usage_stats()
@@ -317,7 +385,7 @@ class NocturnalCLI:
317
385
  """Collect feedback from the user and store it locally"""
318
386
  self.console.print(
319
387
  Panel(
320
- "Share whats working, what feels rough, or any paper/finance workflows you wish existed.\n"
388
+ "Share what's working, what feels rough, or any paper/finance workflows you wish existed.\n"
321
389
  "Press Enter on an empty line to finish.",
322
390
  title="📝 Beta Feedback",
323
391
  border_style="cyan",
@@ -348,7 +416,7 @@ class NocturnalCLI:
348
416
 
349
417
  content = "\n".join(lines)
350
418
  with open(feedback_path, "w", encoding="utf-8") as handle:
351
- handle.write("# Nocturnal Archive Beta Feedback\n")
419
+ handle.write("# Cite Agent Feedback\n")
352
420
  handle.write(f"timestamp = {timestamp}Z\n")
353
421
  handle.write("\n")
354
422
  handle.write(content)
@@ -360,10 +428,184 @@ class NocturnalCLI:
360
428
  self.console.print("[dim]Attach that file when you send feedback to the team.[/dim]")
361
429
  return 0
362
430
 
431
+ def list_library(self, tag: Optional[str] = None):
432
+ """List papers in local library"""
433
+ papers = self.workflow.list_papers(tag=tag)
434
+
435
+ if not papers:
436
+ self.console.print("[warning]No papers in library yet.[/warning]")
437
+ self.console.print("[dim]Use --save-paper after a search to add papers.[/dim]")
438
+ return
439
+
440
+ table = Table(title=f"📚 Library ({len(papers)} papers)", box=box.ROUNDED)
441
+ table.add_column("ID", style="cyan")
442
+ table.add_column("Title", style="bold")
443
+ table.add_column("Authors", style="dim")
444
+ table.add_column("Year", justify="right")
445
+ table.add_column("Tags", style="yellow")
446
+
447
+ for paper in papers[:20]: # Show first 20
448
+ authors_str = paper.authors[0] if paper.authors else "Unknown"
449
+ if len(paper.authors) > 1:
450
+ authors_str += " et al."
451
+
452
+ tags_str = ", ".join(paper.tags) if paper.tags else ""
453
+
454
+ table.add_row(
455
+ paper.paper_id[:8],
456
+ paper.title[:50] + "..." if len(paper.title) > 50 else paper.title,
457
+ authors_str,
458
+ str(paper.year),
459
+ tags_str
460
+ )
461
+
462
+ self.console.print(table)
463
+
464
+ if len(papers) > 20:
465
+ self.console.print(f"[dim]... and {len(papers) - 20} more papers[/dim]")
466
+
467
+ def export_library_bibtex(self):
468
+ """Export library to BibTeX"""
469
+ success = self.workflow.export_to_bibtex()
470
+ if success:
471
+ self.console.print(f"[success]✅ Exported to:[/success] [bold]{self.workflow.bibtex_file}[/bold]")
472
+ self.console.print("[dim]Import this file into Zotero, Mendeley, or any citation manager.[/dim]")
473
+ else:
474
+ self.console.print("[error]❌ Failed to export BibTeX[/error]")
475
+
476
+ def export_library_markdown(self):
477
+ """Export library to Markdown"""
478
+ success = self.workflow.export_to_markdown()
479
+ if success:
480
+ export_file = self.workflow.exports_dir / f"papers_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
481
+ self.console.print(f"[success]✅ Exported to:[/success] [bold]{export_file}[/bold]")
482
+ self.console.print("[dim]Open in Obsidian, Notion, or any markdown editor.[/dim]")
483
+ else:
484
+ self.console.print("[error]❌ Failed to export Markdown[/error]")
485
+
486
+ def show_history(self, limit: int = 10):
487
+ """Show recent query history"""
488
+ history = self.workflow.get_history()[:limit]
489
+
490
+ if not history:
491
+ self.console.print("[warning]No query history yet.[/warning]")
492
+ return
493
+
494
+ table = Table(title=f"📜 Recent Queries", box=box.ROUNDED)
495
+ table.add_column("Time", style="cyan")
496
+ table.add_column("Query", style="bold")
497
+ table.add_column("Tools", style="dim")
498
+
499
+ for entry in history:
500
+ timestamp = datetime.fromisoformat(entry['timestamp'])
501
+ time_str = timestamp.strftime("%m/%d %H:%M")
502
+ query_str = entry['query'][:60] + "..." if len(entry['query']) > 60 else entry['query']
503
+ tools_str = ", ".join(entry.get('metadata', {}).get('tools_used', []))
504
+
505
+ table.add_row(time_str, query_str, tools_str)
506
+
507
+ self.console.print(table)
508
+
509
+ def search_library_interactive(self, query: str):
510
+ """Search papers in library"""
511
+ results = self.workflow.search_library(query)
512
+
513
+ if not results:
514
+ self.console.print(f"[warning]No papers found matching '{query}'[/warning]")
515
+ return
516
+
517
+ self.console.print(f"[success]Found {len(results)} paper(s)[/success]\n")
518
+
519
+ for i, paper in enumerate(results, 1):
520
+ self.console.print(f"[bold cyan]{i}. {paper.title}[/bold cyan]")
521
+ authors_str = ", ".join(paper.authors) if paper.authors else "Unknown"
522
+ self.console.print(f" Authors: {authors_str}")
523
+ self.console.print(f" Year: {paper.year} | ID: {paper.paper_id[:8]}")
524
+ if paper.tags:
525
+ self.console.print(f" Tags: {', '.join(paper.tags)}")
526
+ self.console.print()
527
+
528
+ async def single_query_with_workflow(self, question: str, save_to_library: bool = False,
529
+ copy_to_clipboard: bool = False, export_format: Optional[str] = None):
530
+ """Process a single query with workflow integration"""
531
+ if not await self.initialize(non_interactive=True):
532
+ return
533
+
534
+ try:
535
+ self.console.print(f"🤖 [bold]Processing[/]: {question}")
536
+ self.console.rule(style="magenta")
537
+
538
+ request = ChatRequest(
539
+ question=question,
540
+ user_id="cli_user",
541
+ conversation_id=self.session_id
542
+ )
543
+
544
+ response = await self.agent.process_request(request)
545
+
546
+ self.console.print(f"\n📝 [bold]Response[/]:\n{response.response}")
547
+
548
+ # Tools used removed for cleaner output
549
+
550
+ if response.tokens_used > 0:
551
+ stats = self.agent.get_usage_stats()
552
+ self.console.print(
553
+ f"\n📊 Tokens used: {response.tokens_used} "
554
+ f"(Daily usage: {stats['usage_percentage']:.1f}%)"
555
+ )
556
+
557
+ # Workflow integrations
558
+ if copy_to_clipboard:
559
+ if self.workflow.copy_to_clipboard(response.response):
560
+ self.console.print("[success]📋 Copied to clipboard[/success]")
561
+
562
+ if export_format:
563
+ if export_format == "bibtex":
564
+ # Try to parse paper from response
565
+ paper = parse_paper_from_response(response.response)
566
+ if paper:
567
+ bibtex = paper.to_bibtex()
568
+ self.console.print(f"\n[bold]BibTeX:[/bold]\n{bibtex}")
569
+ if copy_to_clipboard:
570
+ self.workflow.copy_to_clipboard(bibtex)
571
+ else:
572
+ self.console.print("[warning]Could not extract paper info for BibTeX[/warning]")
573
+
574
+ elif export_format == "apa":
575
+ paper = parse_paper_from_response(response.response)
576
+ if paper:
577
+ apa = paper.to_apa_citation()
578
+ self.console.print(f"\n[bold]APA Citation:[/bold]\n{apa}")
579
+ if copy_to_clipboard:
580
+ self.workflow.copy_to_clipboard(apa)
581
+
582
+ # Save to history
583
+ self.workflow.save_query_result(
584
+ query=question,
585
+ response=response.response,
586
+ metadata={
587
+ "tools_used": response.tools_used,
588
+ "tokens_used": response.tokens_used,
589
+ "confidence_score": response.confidence_score
590
+ }
591
+ )
592
+
593
+ if save_to_library:
594
+ paper = parse_paper_from_response(response.response)
595
+ if paper:
596
+ if self.workflow.add_paper(paper):
597
+ self.console.print(f"[success]✅ Saved to library (ID: {paper.paper_id[:8]})[/success]")
598
+ else:
599
+ self.console.print("[error]❌ Failed to save to library[/error]")
600
+
601
+ finally:
602
+ if self.agent:
603
+ await self.agent.close()
604
+
363
605
  def main():
364
606
  """Main CLI entry point"""
365
607
  parser = argparse.ArgumentParser(
366
- description="Nocturnal Archive - AI Research Assistant",
608
+ description="Cite Agent - AI Research Assistant with real data",
367
609
  formatter_class=argparse.RawDescriptionHelpFormatter,
368
610
  epilog="""
369
611
  Examples:
@@ -419,10 +661,16 @@ Examples:
419
661
  )
420
662
 
421
663
  parser.add_argument(
422
- '--feedback',
423
- action='store_true',
664
+ '--feedback',
665
+ action='store_true',
424
666
  help='Capture beta feedback and save it locally'
425
667
  )
668
+
669
+ parser.add_argument(
670
+ '--workflow',
671
+ action='store_true',
672
+ help='Start workflow mode for integrated research management'
673
+ )
426
674
 
427
675
  parser.add_argument(
428
676
  '--import-secrets',
@@ -436,11 +684,66 @@ Examples:
436
684
  help='Fail secret import if keyring is unavailable'
437
685
  )
438
686
 
687
+ # Workflow integration arguments
688
+ parser.add_argument(
689
+ '--library',
690
+ action='store_true',
691
+ help='List all papers in local library'
692
+ )
693
+
694
+ parser.add_argument(
695
+ '--export-bibtex',
696
+ action='store_true',
697
+ help='Export library to BibTeX format'
698
+ )
699
+
700
+ parser.add_argument(
701
+ '--export-markdown',
702
+ action='store_true',
703
+ help='Export library to Markdown format'
704
+ )
705
+
706
+ parser.add_argument(
707
+ '--history',
708
+ action='store_true',
709
+ help='Show recent query history'
710
+ )
711
+
712
+ parser.add_argument(
713
+ '--search-library',
714
+ metavar='QUERY',
715
+ help='Search papers in local library'
716
+ )
717
+
718
+ parser.add_argument(
719
+ '--save',
720
+ action='store_true',
721
+ help='Save query results to library (use with query)'
722
+ )
723
+
724
+ parser.add_argument(
725
+ '--copy',
726
+ action='store_true',
727
+ help='Copy results to clipboard (use with query)'
728
+ )
729
+
730
+ parser.add_argument(
731
+ '--format',
732
+ choices=['bibtex', 'apa', 'markdown'],
733
+ help='Export format for citations (use with query)'
734
+ )
735
+
736
+ parser.add_argument(
737
+ '--tag',
738
+ metavar='TAG',
739
+ help='Filter library by tag'
740
+ )
741
+
439
742
  args = parser.parse_args()
440
743
 
441
744
  # Handle version
442
745
  if args.version:
443
- print("Nocturnal Archive v1.0.0")
746
+ print("Cite Agent v1.2.2")
444
747
  print("AI Research Assistant with real data integration")
445
748
  return
446
749
 
@@ -454,6 +757,29 @@ Examples:
454
757
  exit_code = cli.collect_feedback()
455
758
  sys.exit(exit_code)
456
759
 
760
+ # Handle workflow commands (no agent initialization needed)
761
+ cli = NocturnalCLI()
762
+
763
+ if args.library:
764
+ cli.list_library(tag=args.tag)
765
+ sys.exit(0)
766
+
767
+ if args.export_bibtex:
768
+ cli.export_library_bibtex()
769
+ sys.exit(0)
770
+
771
+ if args.export_markdown:
772
+ cli.export_library_markdown()
773
+ sys.exit(0)
774
+
775
+ if args.history:
776
+ cli.show_history(limit=20)
777
+ sys.exit(0)
778
+
779
+ if args.search_library:
780
+ cli.search_library_interactive(args.search_library)
781
+ sys.exit(0)
782
+
457
783
  # Handle secret import before setup as it can be used non-interactively
458
784
  if args.import_secrets:
459
785
  config = NocturnalConfig()
@@ -492,12 +818,21 @@ Examples:
492
818
 
493
819
  # Handle query or interactive mode
494
820
  async def run_cli():
495
- cli = NocturnalCLI()
821
+ cli_instance = NocturnalCLI()
496
822
 
497
823
  if args.query and not args.interactive:
498
- await cli.single_query(args.query)
824
+ # Check if workflow flags are set
825
+ if args.save or args.copy or args.format:
826
+ await cli_instance.single_query_with_workflow(
827
+ args.query,
828
+ save_to_library=args.save,
829
+ copy_to_clipboard=args.copy,
830
+ export_format=args.format
831
+ )
832
+ else:
833
+ await cli_instance.single_query(args.query)
499
834
  else:
500
- await cli.interactive_mode()
835
+ await cli_instance.interactive_mode()
501
836
 
502
837
  try:
503
838
  asyncio.run(run_cli())