cite-agent 1.0.4__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.

Files changed (48) hide show
  1. cite_agent/__init__.py +1 -1
  2. cite_agent/account_client.py +19 -46
  3. cite_agent/agent_backend_only.py +30 -4
  4. cite_agent/cli.py +397 -64
  5. cite_agent/cli_conversational.py +294 -0
  6. cite_agent/cli_workflow.py +276 -0
  7. cite_agent/enhanced_ai_agent.py +3222 -117
  8. cite_agent/session_manager.py +215 -0
  9. cite_agent/setup_config.py +5 -21
  10. cite_agent/streaming_ui.py +252 -0
  11. cite_agent/updater.py +50 -17
  12. cite_agent/workflow.py +427 -0
  13. cite_agent/workflow_integration.py +275 -0
  14. cite_agent-1.2.3.dist-info/METADATA +442 -0
  15. cite_agent-1.2.3.dist-info/RECORD +54 -0
  16. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/top_level.txt +1 -0
  17. src/__init__.py +1 -0
  18. src/services/__init__.py +132 -0
  19. src/services/auth_service/__init__.py +3 -0
  20. src/services/auth_service/auth_manager.py +33 -0
  21. src/services/graph/__init__.py +1 -0
  22. src/services/graph/knowledge_graph.py +194 -0
  23. src/services/llm_service/__init__.py +5 -0
  24. src/services/llm_service/llm_manager.py +495 -0
  25. src/services/paper_service/__init__.py +5 -0
  26. src/services/paper_service/openalex.py +231 -0
  27. src/services/performance_service/__init__.py +1 -0
  28. src/services/performance_service/rust_performance.py +395 -0
  29. src/services/research_service/__init__.py +23 -0
  30. src/services/research_service/chatbot.py +2056 -0
  31. src/services/research_service/citation_manager.py +436 -0
  32. src/services/research_service/context_manager.py +1441 -0
  33. src/services/research_service/conversation_manager.py +597 -0
  34. src/services/research_service/critical_paper_detector.py +577 -0
  35. src/services/research_service/enhanced_research.py +121 -0
  36. src/services/research_service/enhanced_synthesizer.py +375 -0
  37. src/services/research_service/query_generator.py +777 -0
  38. src/services/research_service/synthesizer.py +1273 -0
  39. src/services/search_service/__init__.py +5 -0
  40. src/services/search_service/indexer.py +186 -0
  41. src/services/search_service/search_engine.py +342 -0
  42. src/services/simple_enhanced_main.py +287 -0
  43. cite_agent/__distribution__.py +0 -7
  44. cite_agent-1.0.4.dist-info/METADATA +0 -234
  45. cite_agent-1.0.4.dist-info/RECORD +0 -23
  46. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/WHEEL +0 -0
  47. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/entry_points.txt +0 -0
  48. {cite_agent-1.0.4.dist-info → cite_agent-1.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ User-Friendly Session Manager for Cite-Agent
4
+ Handles session detection, user choices, and authentication flow
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Optional, Dict, Any
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.prompt import Prompt, Confirm
15
+ from rich.table import Table
16
+ from rich.text import Text
17
+
18
+ class SessionManager:
19
+ """User-friendly session management for Cite-Agent"""
20
+
21
+ def __init__(self):
22
+ self.console = Console()
23
+ self.session_file = Path.home() / ".nocturnal_archive" / "session.json"
24
+ self.config_file = Path.home() / ".nocturnal_archive" / "config.env"
25
+ self.session_data: Optional[Dict[str, Any]] = None
26
+
27
+ def detect_existing_session(self) -> bool:
28
+ """Detect if there's an existing session and load it"""
29
+ if not self.session_file.exists():
30
+ return False
31
+
32
+ try:
33
+ with open(self.session_file, 'r') as f:
34
+ self.session_data = json.load(f)
35
+ return True
36
+ except Exception as e:
37
+ self.console.print(f"[red]⚠️ Session file corrupted: {e}[/red]")
38
+ return False
39
+
40
+ def show_session_info(self):
41
+ """Display existing session information in a user-friendly way"""
42
+ if not self.session_data:
43
+ return
44
+
45
+ email = self.session_data.get('email', 'Unknown')
46
+ user_id = self.session_data.get('user_id', 'Unknown')[:8] + "..."
47
+ expires_at = self.session_data.get('expires_at', 'Unknown')
48
+ daily_limit = self.session_data.get('daily_token_limit', 0)
49
+
50
+ # Create a nice table
51
+ table = Table(title="🔑 Existing Session Found", show_header=True, header_style="bold green")
52
+ table.add_column("Property", style="cyan", width=15)
53
+ table.add_column("Value", style="white")
54
+
55
+ table.add_row("Email", email)
56
+ table.add_row("User ID", user_id)
57
+ table.add_row("Daily Limit", f"{daily_limit:,} queries")
58
+ table.add_row("Expires", expires_at)
59
+
60
+ self.console.print()
61
+ self.console.print(table)
62
+ self.console.print()
63
+
64
+ def ask_session_choice(self) -> str:
65
+ """Ask user what they want to do with the existing session"""
66
+ self.console.print("[bold cyan]What would you like to do?[/bold cyan]")
67
+ self.console.print()
68
+
69
+ # Create a nice menu
70
+ menu_table = Table(show_header=False, box=None, padding=(0, 1))
71
+ menu_table.add_column("Choice", style="bold green", width=3)
72
+ menu_table.add_column("Action", style="white", width=20)
73
+ menu_table.add_column("Description", style="dim", width=40)
74
+
75
+ menu_table.add_row("1", "Resume", "Continue with this session")
76
+ menu_table.add_row("2", "Switch", "Login with different account")
77
+ menu_table.add_row("3", "Logout", "Clear session and start fresh")
78
+ menu_table.add_row("4", "Help", "Show session management help")
79
+
80
+ self.console.print(menu_table)
81
+ self.console.print()
82
+
83
+ while True:
84
+ choice = Prompt.ask(
85
+ "Choose an option",
86
+ choices=["1", "2", "3", "4", "resume", "switch", "logout", "help"],
87
+ default="1"
88
+ ).lower()
89
+
90
+ if choice in ["1", "resume"]:
91
+ return "resume"
92
+ elif choice in ["2", "switch"]:
93
+ return "switch"
94
+ elif choice in ["3", "logout"]:
95
+ return "logout"
96
+ elif choice in ["4", "help"]:
97
+ self.show_help()
98
+ continue
99
+ else:
100
+ self.console.print("[red]Invalid choice. Please try again.[/red]")
101
+
102
+ def show_help(self):
103
+ """Show help for session management"""
104
+ help_text = """
105
+ [bold cyan]Session Management Help[/bold cyan]
106
+
107
+ [bold green]Resume:[/bold green] Continue with your existing session
108
+ • Use your current login and settings
109
+ • No need to re-authenticate
110
+ • All your data and preferences are preserved
111
+
112
+ [bold yellow]Switch:[/bold yellow] Login with a different account
113
+ • Logout from current session
114
+ • Start fresh with new account
115
+ • Previous session data will be cleared
116
+
117
+ [bold red]Logout:[/bold red] Clear session and start fresh
118
+ • Remove all saved login information
119
+ • Start completely fresh
120
+ • You'll need to login again
121
+
122
+ [bold blue]Session Files:[/bold blue]
123
+ • Session: ~/.nocturnal_archive/session.json
124
+ • Config: ~/.nocturnal_archive/config.env
125
+
126
+ [bold blue]Manual Session Management:[/bold blue]
127
+ • To clear session manually: rm ~/.nocturnal_archive/session.json
128
+ • To clear config: rm ~/.nocturnal_archive/config.env
129
+ """
130
+
131
+ self.console.print(Panel(help_text, title="Help", border_style="blue"))
132
+ self.console.print()
133
+
134
+ def clear_session(self) -> bool:
135
+ """Clear the existing session"""
136
+ try:
137
+ if self.session_file.exists():
138
+ self.session_file.unlink()
139
+ if self.config_file.exists():
140
+ self.config_file.unlink()
141
+ self.console.print("[green]✅ Session cleared successfully[/green]")
142
+ return True
143
+ except Exception as e:
144
+ self.console.print(f"[red]❌ Error clearing session: {e}[/red]")
145
+ return False
146
+
147
+ def handle_session_affirmation(self) -> str:
148
+ """Main function to handle session affirmation with user-friendly interface"""
149
+ # Check for existing session
150
+ has_session = self.detect_existing_session()
151
+
152
+ if not has_session:
153
+ self.console.print("[yellow]No existing session found. Starting fresh...[/yellow]")
154
+ return "fresh"
155
+
156
+ # Show session information
157
+ self.show_session_info()
158
+
159
+ # Ask user what to do
160
+ choice = self.ask_session_choice()
161
+
162
+ if choice == "resume":
163
+ self.console.print("[green]✅ Resuming existing session...[/green]")
164
+ return "resume"
165
+ elif choice == "switch":
166
+ self.console.print("[yellow]🔄 Switching to different account...[/yellow]")
167
+ if self.clear_session():
168
+ return "fresh"
169
+ else:
170
+ return "error"
171
+ elif choice == "logout":
172
+ self.console.print("[red]🚪 Logging out...[/red]")
173
+ if self.clear_session():
174
+ return "fresh"
175
+ else:
176
+ return "error"
177
+
178
+ return "error"
179
+
180
+ def setup_environment_variables(self):
181
+ """Set up environment variables for backend mode"""
182
+ # PRODUCTION MODE: Force backend, ensure monetization
183
+ # NEVER load user's .env files in production
184
+
185
+ # Set backend URL if not already set
186
+ if "NOCTURNAL_API_URL" not in os.environ:
187
+ os.environ["NOCTURNAL_API_URL"] = "https://cite-agent-api-720dfadd602c.herokuapp.com/api"
188
+
189
+ # SECURITY: Default to backend mode (USE_LOCAL_KEYS=false)
190
+ # This ensures users MUST authenticate and pay
191
+ if "USE_LOCAL_KEYS" not in os.environ:
192
+ os.environ["USE_LOCAL_KEYS"] = "false"
193
+
194
+ def get_session_status(self) -> Dict[str, Any]:
195
+ """Get current session status for debugging"""
196
+ return {
197
+ "session_file_exists": self.session_file.exists(),
198
+ "config_file_exists": self.config_file.exists(),
199
+ "session_data": self.session_data,
200
+ "use_local_keys": os.environ.get("USE_LOCAL_KEYS", "not set"),
201
+ "api_url": os.environ.get("NOCTURNAL_API_URL", "not set")
202
+ }
203
+
204
+ def main():
205
+ """Test the session manager"""
206
+ sm = SessionManager()
207
+ result = sm.handle_session_affirmation()
208
+ print(f"Result: {result}")
209
+
210
+ # Show status
211
+ status = sm.get_session_status()
212
+ print(f"Status: {status}")
213
+
214
+ if __name__ == "__main__":
215
+ main()
@@ -63,18 +63,6 @@ class NocturnalConfig:
63
63
  print("You'll use your institution-issued account to sign in. No invite codes or manual API keys required.")
64
64
  print()
65
65
 
66
- # Ask if new user or returning user
67
- print("Are you a new user or returning user?")
68
- print(" 1. New user (register)")
69
- print(" 2. Returning user (login)")
70
- choice = input("Enter choice (1 or 2): ").strip()
71
-
72
- is_new_user = choice == "1"
73
- action = "Registration" if is_new_user else "Login"
74
-
75
- print(f"\n{action}")
76
- print("-" * 40)
77
-
78
66
  email = self._prompt_academic_email()
79
67
  if not email:
80
68
  return False
@@ -83,18 +71,14 @@ class NocturnalConfig:
83
71
  if not password:
84
72
  return False
85
73
 
86
- if is_new_user and not self._confirm_beta_terms():
74
+ if not self._confirm_beta_terms():
87
75
  print("❌ Terms must be accepted to continue")
88
76
  return False
89
77
 
90
78
  try:
91
- credentials = self._provision_account(email, password, is_new_user=is_new_user)
92
- if is_new_user:
93
- print(f"\n✅ Account created successfully for {email}")
94
- else:
95
- print(f"\n✅ Logged in successfully as {email}")
79
+ credentials = self._provision_account(email, password)
96
80
  except AccountProvisioningError as exc:
97
- print(f"❌ {action} failed: {exc}")
81
+ print(f"❌ Could not verify your account: {exc}")
98
82
  return False
99
83
 
100
84
  print("\n🛡️ Recap of beta limitations:")
@@ -179,9 +163,9 @@ class NocturnalConfig:
179
163
  print("❌ Could not confirm password after multiple attempts")
180
164
  return None
181
165
 
182
- def _provision_account(self, email: str, password: str, is_new_user: bool = False) -> AccountCredentials:
166
+ def _provision_account(self, email: str, password: str) -> AccountCredentials:
183
167
  client = AccountClient()
184
- return client.provision(email=email, password=password, is_new_user=is_new_user)
168
+ return client.provision(email=email, password=password)
185
169
 
186
170
  def _is_academic_email(self, email: str) -> bool:
187
171
  if "@" not in email:
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Streaming Chat UI - Cursor/Claude Style Interface
4
+ Minimal, clean, conversational interface for data analysis assistant
5
+ """
6
+
7
+ import sys
8
+ import time
9
+ import asyncio
10
+ from typing import Optional, AsyncGenerator
11
+ from rich.console import Console
12
+ from rich.markdown import Markdown
13
+ from rich.text import Text
14
+ from rich.live import Live
15
+ from rich.spinner import Spinner
16
+
17
+ console = Console()
18
+
19
+
20
+ class StreamingChatUI:
21
+ """
22
+ Clean, minimal chat interface matching Cursor/Claude aesthetics
23
+ - Simple header (just app name)
24
+ - "You:" / "Agent:" conversation labels
25
+ - Streaming character-by-character output
26
+ - Transient action indicators
27
+ - Markdown rendering for rich text
28
+ """
29
+
30
+ def __init__(self, app_name: str = "Nocturnal Archive", working_dir: Optional[str] = None):
31
+ self.app_name = app_name
32
+ self.working_dir = working_dir
33
+ self.console = Console()
34
+ self.typing_speed = 0.015 # ~60 chars/sec
35
+
36
+ def show_header(self):
37
+ """Display minimal header on startup"""
38
+ self.console.print(f"\n[bold cyan]{self.app_name}[/bold cyan]")
39
+ if self.working_dir:
40
+ self.console.print(f"[dim]Connected to: {self.working_dir}[/dim]")
41
+ self.console.print("─" * 70)
42
+ self.console.print()
43
+
44
+ def show_user_message(self, message: str):
45
+ """Display user message with 'You:' prefix"""
46
+ self.console.print(f"[bold]You:[/bold] {message}")
47
+ self.console.print()
48
+
49
+ async def stream_agent_response(
50
+ self,
51
+ content_generator: AsyncGenerator[str, None],
52
+ show_markdown: bool = True
53
+ ):
54
+ """
55
+ Stream agent response character-by-character
56
+
57
+ Args:
58
+ content_generator: Async generator yielding text chunks
59
+ show_markdown: Whether to render as markdown (default True)
60
+ """
61
+ # No prefix for agent - just stream naturally
62
+ buffer = ""
63
+
64
+ async for chunk in content_generator:
65
+ buffer += chunk
66
+ # Stream character by character for natural feel
67
+ for char in chunk:
68
+ self.console.print(char, end="", style="white")
69
+ await asyncio.sleep(self.typing_speed)
70
+
71
+ self.console.print() # Newline after response
72
+ self.console.print() # Extra space for readability
73
+
74
+ return buffer
75
+
76
+ async def stream_markdown_response(self, markdown_text: str):
77
+ """
78
+ Stream a markdown response with proper formatting
79
+ Used for final rendering after streaming is complete
80
+ """
81
+ # Render markdown with Rich
82
+ md = Markdown(markdown_text)
83
+ self.console.print(md)
84
+ self.console.print()
85
+
86
+ def show_action_indicator(self, action: str) -> Live:
87
+ """
88
+ Show a transient action indicator (e.g., [reading file...])
89
+ Returns Live object that should be stopped when action completes
90
+
91
+ Usage:
92
+ indicator = ui.show_action_indicator("analyzing data")
93
+ # ... do work ...
94
+ indicator.stop()
95
+ """
96
+ spinner = Spinner("dots", text=f"[dim]{action}[/dim]")
97
+ live = Live(spinner, console=self.console, transient=True)
98
+ live.start()
99
+ return live
100
+
101
+ def show_error(self, error_message: str):
102
+ """Display error message"""
103
+ self.console.print(f"[red]Error:[/red] {error_message}")
104
+ self.console.print()
105
+
106
+ def show_info(self, message: str):
107
+ """Display info message"""
108
+ self.console.print(f"[dim]{message}[/dim]")
109
+ self.console.print()
110
+
111
+ def show_rate_limit_message(
112
+ self,
113
+ limit_type: str = "Archive API",
114
+ remaining_capabilities: Optional[list] = None
115
+ ):
116
+ """
117
+ Show soft degradation message when rate limited
118
+
119
+ Args:
120
+ limit_type: What service is limited (e.g., "Archive API")
121
+ remaining_capabilities: List of what's still available
122
+ """
123
+ self.console.print(
124
+ f"\n[yellow]I've reached the daily limit for {limit_type} queries.[/yellow]\n"
125
+ )
126
+
127
+ if remaining_capabilities:
128
+ self.console.print("[bold]However, I can still assist you with:[/bold]")
129
+ for capability in remaining_capabilities:
130
+ self.console.print(f" • {capability}")
131
+ self.console.print()
132
+
133
+ self.console.print(
134
+ "[dim]For unlimited access, consider upgrading to Pro.[/dim]\n"
135
+ )
136
+
137
+ def get_user_input(self, prompt: str = "You: ") -> str:
138
+ """Get user input with custom prompt"""
139
+ try:
140
+ user_input = self.console.input(f"[bold]{prompt}[/bold]")
141
+ self.console.print()
142
+ return user_input.strip()
143
+ except (KeyboardInterrupt, EOFError):
144
+ self.console.print("\n[dim]Goodbye![/dim]")
145
+ sys.exit(0)
146
+
147
+ def clear_screen(self):
148
+ """Clear terminal screen"""
149
+ self.console.clear()
150
+
151
+
152
+ # Utility functions for streaming from Groq API
153
+
154
+ async def groq_stream_to_generator(stream) -> AsyncGenerator[str, None]:
155
+ """
156
+ Convert Groq streaming response to async generator
157
+
158
+ Args:
159
+ stream: Groq stream object from client.chat.completions.create(stream=True)
160
+
161
+ Yields:
162
+ Text chunks from the stream
163
+ """
164
+ for chunk in stream:
165
+ if chunk.choices and chunk.choices[0].delta.content:
166
+ yield chunk.choices[0].delta.content
167
+
168
+
169
+ async def simulate_streaming(text: str, chunk_size: int = 5) -> AsyncGenerator[str, None]:
170
+ """
171
+ Simulate streaming for testing purposes
172
+
173
+ Args:
174
+ text: Full text to stream
175
+ chunk_size: Characters per chunk
176
+
177
+ Yields:
178
+ Text chunks
179
+ """
180
+ for i in range(0, len(text), chunk_size):
181
+ chunk = text[i:i + chunk_size]
182
+ yield chunk
183
+ await asyncio.sleep(0.05) # Simulate network delay
184
+
185
+
186
+ # Example usage
187
+ async def example_usage():
188
+ """Example of how to use the streaming UI"""
189
+
190
+ ui = StreamingChatUI(
191
+ app_name="Nocturnal Archive",
192
+ working_dir="/home/researcher/project"
193
+ )
194
+
195
+ # Show header on startup
196
+ ui.show_header()
197
+
198
+ # Simulate conversation
199
+ ui.show_user_message("hello")
200
+
201
+ # Simulate streaming response
202
+ response_text = (
203
+ "Good evening. I'm ready to assist with your analysis. "
204
+ "What would you like to work on today?"
205
+ )
206
+
207
+ async def response_generator():
208
+ async for chunk in simulate_streaming(response_text):
209
+ yield chunk
210
+
211
+ await ui.stream_agent_response(response_generator())
212
+
213
+ # Get next user input
214
+ user_input = ui.get_user_input()
215
+ ui.show_user_message(user_input)
216
+
217
+ # Show action indicator
218
+ indicator = ui.show_action_indicator("reading file")
219
+ await asyncio.sleep(2) # Simulate work
220
+ indicator.stop()
221
+
222
+ # Stream another response with markdown
223
+ markdown_response = """
224
+ I can see you have several data files here:
225
+
226
+ • **gdp_data_2020_2024.csv** (245 KB)
227
+ • **unemployment_rates.xlsx** (89 KB)
228
+
229
+ Which dataset would you like me to analyze first?
230
+ """
231
+
232
+ async def md_generator():
233
+ async for chunk in simulate_streaming(markdown_response):
234
+ yield chunk
235
+
236
+ await ui.stream_agent_response(md_generator())
237
+
238
+ # Show rate limit message
239
+ ui.show_rate_limit_message(
240
+ limit_type="Archive API",
241
+ remaining_capabilities=[
242
+ "Local data analysis (unlimited)",
243
+ "Web searches (unlimited)",
244
+ "Financial data (5 queries remaining)",
245
+ "Conversation and file reading"
246
+ ]
247
+ )
248
+
249
+
250
+ if __name__ == "__main__":
251
+ # Run example
252
+ asyncio.run(example_usage())
cite_agent/updater.py CHANGED
@@ -12,18 +12,29 @@ from pathlib import Path
12
12
  from typing import Optional, Dict, Any
13
13
 
14
14
  try:
15
- import pkg_resources
15
+ # Use modern importlib.metadata instead of deprecated pkg_resources
16
+ from importlib.metadata import version as get_version
17
+ pkg_resources = None # Not needed anymore
16
18
  except ImportError:
17
- pkg_resources = None
19
+ # Fallback for Python < 3.8
20
+ try:
21
+ import warnings
22
+ with warnings.catch_warnings():
23
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
24
+ warnings.filterwarnings("ignore", category=UserWarning)
25
+ import pkg_resources
26
+ except ImportError:
27
+ pkg_resources = None
28
+ get_version = None
18
29
 
19
30
  class NocturnalUpdater:
20
31
  """Handles automatic updates for Nocturnal Archive"""
21
32
 
22
33
  def __init__(self):
23
34
  self.current_version = self.get_current_version()
24
- self.package_name = "nocturnal-archive"
35
+ self.package_name = "cite-agent" # Fixed: was "nocturnal-archive"
25
36
  self.pypi_url = f"https://pypi.org/pypi/{self.package_name}/json"
26
- self.kill_switch_url = "https://api.nocturnal.dev/api/admin/status"
37
+ self.kill_switch_url = "https://cite-agent-api-720dfadd602c.herokuapp.com/api/health"
27
38
 
28
39
  def check_kill_switch(self) -> Dict[str, Any]:
29
40
  """Check if kill switch is activated"""
@@ -37,16 +48,23 @@ class NocturnalUpdater:
37
48
 
38
49
  def get_current_version(self) -> str:
39
50
  """Get current installed version"""
51
+ # Try modern importlib.metadata first
52
+ try:
53
+ return get_version(self.package_name)
54
+ except Exception:
55
+ pass
56
+
57
+ # Fallback to pkg_resources (deprecated)
40
58
  if pkg_resources:
41
59
  try:
42
60
  return pkg_resources.get_distribution(self.package_name).version
43
- except (pkg_resources.DistributionNotFound, Exception):
61
+ except Exception:
44
62
  pass
45
63
 
46
- # Fallback: try to get version from installed package
64
+ # Last resort: try to get version from installed package
47
65
  try:
48
- import nocturnal_archive
49
- return getattr(nocturnal_archive, '__version__', '1.0.0')
66
+ import cite_agent
67
+ return getattr(cite_agent, '__version__', '1.0.0')
50
68
  except ImportError:
51
69
  return "1.0.0"
52
70
 
@@ -103,32 +121,47 @@ class NocturnalUpdater:
103
121
  except:
104
122
  return False
105
123
 
106
- def update_package(self, force: bool = False) -> bool:
124
+ def update_package(self, force: bool = False, silent: bool = False) -> bool:
107
125
  """Update the package to latest version"""
108
126
  try:
109
- print("🔄 Updating Nocturnal Archive...")
127
+ if not silent:
128
+ print("🔄 Updating cite-agent...")
110
129
 
111
130
  # Check if update is needed
112
131
  if not force:
113
132
  update_info = self.check_for_updates()
114
133
  if not update_info or not update_info["available"]:
115
- print("✅ No updates available")
134
+ if not silent:
135
+ print("✅ No updates available")
116
136
  return True
117
137
 
118
- # Perform update
119
- cmd = [sys.executable, "-m", "pip", "install", "--upgrade", self.package_name]
138
+ # Perform update with user flag to avoid system package conflicts
139
+ cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "--user", self.package_name]
120
140
  result = subprocess.run(cmd, capture_output=True, text=True)
121
141
 
122
142
  if result.returncode == 0:
123
- new_version = self.get_current_version()
124
- print(f"✅ Updated to version {new_version}")
143
+ # Create flag file to notify next launch
144
+ try:
145
+ from pathlib import Path
146
+ update_flag = Path.home() / ".nocturnal_archive" / ".updated"
147
+ update_flag.parent.mkdir(exist_ok=True)
148
+ update_flag.write_text(self.get_current_version())
149
+ except:
150
+ pass
151
+
152
+ if not silent:
153
+ new_version = self.get_current_version()
154
+ print(f"✅ Updated to version {new_version}")
155
+ print("🔄 Restart cite-agent to use the new version")
125
156
  return True
126
157
  else:
127
- print(f"❌ Update failed: {result.stderr}")
158
+ if not silent:
159
+ print(f"❌ Update failed: {result.stderr}")
128
160
  return False
129
161
 
130
162
  except Exception as e:
131
- print(f"❌ Update error: {e}")
163
+ if not silent:
164
+ print(f"❌ Update error: {e}")
132
165
  return False
133
166
 
134
167
  def show_update_status(self):