cite-agent 1.0.0__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/__distribution__.py +7 -0
- cite_agent/__init__.py +66 -0
- cite_agent/account_client.py +130 -0
- cite_agent/agent_backend_only.py +172 -0
- cite_agent/ascii_plotting.py +296 -0
- cite_agent/auth.py +281 -0
- cite_agent/backend_only_client.py +83 -0
- cite_agent/cli.py +512 -0
- cite_agent/cli_enhanced.py +207 -0
- cite_agent/dashboard.py +339 -0
- cite_agent/enhanced_ai_agent.py +172 -0
- cite_agent/rate_limiter.py +298 -0
- cite_agent/setup_config.py +417 -0
- cite_agent/telemetry.py +85 -0
- cite_agent/ui.py +175 -0
- cite_agent/updater.py +187 -0
- cite_agent/web_search.py +203 -0
- cite_agent-1.0.0.dist-info/METADATA +234 -0
- cite_agent-1.0.0.dist-info/RECORD +23 -0
- cite_agent-1.0.0.dist-info/WHEEL +5 -0
- cite_agent-1.0.0.dist-info/entry_points.txt +3 -0
- cite_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
- cite_agent-1.0.0.dist-info/top_level.txt +1 -0
cite_agent/cli.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Nocturnal Archive CLI - Command Line Interface
|
|
4
|
+
Provides a terminal interface similar to cursor-agent
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import random
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from rich import box
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.table import Table
|
|
21
|
+
from rich.theme import Theme
|
|
22
|
+
|
|
23
|
+
from .enhanced_ai_agent import EnhancedNocturnalAgent, ChatRequest
|
|
24
|
+
from .setup_config import NocturnalConfig, DEFAULT_QUERY_LIMIT, MANAGED_SECRETS
|
|
25
|
+
from .telemetry import TelemetryManager
|
|
26
|
+
from .updater import NocturnalUpdater
|
|
27
|
+
|
|
28
|
+
class NocturnalCLI:
|
|
29
|
+
"""Command Line Interface for Nocturnal Archive"""
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self.agent: Optional[EnhancedNocturnalAgent] = None
|
|
33
|
+
self.session_id = f"cli_{os.getpid()}"
|
|
34
|
+
self.telemetry = None
|
|
35
|
+
self.console = Console(theme=Theme({
|
|
36
|
+
"banner": "bold magenta",
|
|
37
|
+
"success": "bold green",
|
|
38
|
+
"warning": "bold yellow",
|
|
39
|
+
"error": "bold red",
|
|
40
|
+
}))
|
|
41
|
+
self._tips = [
|
|
42
|
+
"Use [bold]nocturnal --setup[/] to rerun the onboarding wizard anytime.",
|
|
43
|
+
"Run [bold]nocturnal tips[/] when you need a refresher on power moves.",
|
|
44
|
+
"Pass a one-off question directly: [bold]nocturnal \"summarize the latest 10-Q\"[/].",
|
|
45
|
+
"Enable verbose logging by exporting [bold]NOCTURNAL_DEBUG=1[/] before launching.",
|
|
46
|
+
"Use [bold]/plan[/] inside the chat to nudge the agent toward structured research steps.",
|
|
47
|
+
"Hit [bold]Ctrl+C[/] to stop a long-running call; the agent will clean up gracefully.",
|
|
48
|
+
"Remember the sandbox: prefix shell commands with [bold]![/] to execute safe utilities only.",
|
|
49
|
+
"If you see an auto-update notice, the CLI will restart itself to load the latest build.",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
async def initialize(self):
|
|
53
|
+
"""Initialize the agent with automatic updates"""
|
|
54
|
+
# Check for update notifications from previous runs
|
|
55
|
+
self._check_update_notification()
|
|
56
|
+
self._show_intro_panel()
|
|
57
|
+
|
|
58
|
+
self._enforce_latest_build()
|
|
59
|
+
|
|
60
|
+
config = NocturnalConfig()
|
|
61
|
+
had_config = config.setup_environment()
|
|
62
|
+
TelemetryManager.refresh()
|
|
63
|
+
self.telemetry = TelemetryManager.get()
|
|
64
|
+
|
|
65
|
+
if not config.check_setup():
|
|
66
|
+
self.console.print("\n[warning]š Hey there, looks like this machine hasn't met Nocturnal yet.[/warning]")
|
|
67
|
+
self.console.print("[banner]Let's get you signed in ā this only takes a minute.[/banner]")
|
|
68
|
+
try:
|
|
69
|
+
if not config.interactive_setup():
|
|
70
|
+
self.console.print("[error]ā Setup was cancelled. Exiting without starting the agent.[/error]")
|
|
71
|
+
return False
|
|
72
|
+
except (KeyboardInterrupt, EOFError):
|
|
73
|
+
self.console.print("\n[error]ā Setup interrupted. Exiting without starting the agent.[/error]")
|
|
74
|
+
return False
|
|
75
|
+
config.setup_environment()
|
|
76
|
+
TelemetryManager.refresh()
|
|
77
|
+
self.telemetry = TelemetryManager.get()
|
|
78
|
+
elif not had_config:
|
|
79
|
+
# config.setup_environment() may have populated env vars from file silently
|
|
80
|
+
self.console.print("[success]āļø Loaded saved credentials for this device.[/success]")
|
|
81
|
+
|
|
82
|
+
self.agent = EnhancedNocturnalAgent()
|
|
83
|
+
success = await self.agent.initialize()
|
|
84
|
+
|
|
85
|
+
if not success:
|
|
86
|
+
self.console.print("[error]ā Failed to initialize agent. Please check your API keys.[/error]")
|
|
87
|
+
self.console.print("\nš” Setup help:")
|
|
88
|
+
self.console.print(" ⢠Ensure your Groq API key is correct (re-run `nocturnal` to update it).")
|
|
89
|
+
self.console.print(" ⢠You can edit ~/.nocturnal_archive/config.env or set GROQ_API_KEY manually.")
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
self._show_ready_panel()
|
|
93
|
+
self._show_beta_banner()
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def _show_beta_banner(self):
|
|
97
|
+
account_email = os.getenv("NOCTURNAL_ACCOUNT_EMAIL", "")
|
|
98
|
+
configured_limit = DEFAULT_QUERY_LIMIT
|
|
99
|
+
if configured_limit <= 0:
|
|
100
|
+
limit_text = "Unlimited"
|
|
101
|
+
else:
|
|
102
|
+
limit_text = f"{configured_limit}"
|
|
103
|
+
details = [
|
|
104
|
+
f"Daily limit: [bold]{limit_text}[/] queries",
|
|
105
|
+
"Telemetry streaming: [bold]enabled[/] (control plane)",
|
|
106
|
+
"Auto-update: [bold]enforced[/] on launch",
|
|
107
|
+
"Sandbox: safe shell commands only ⢠SQL workflows supported",
|
|
108
|
+
]
|
|
109
|
+
if account_email:
|
|
110
|
+
details.insert(0, f"Signed in as: [bold]{account_email}[/]")
|
|
111
|
+
|
|
112
|
+
panel = Panel(
|
|
113
|
+
"\n".join(details),
|
|
114
|
+
title="šļø Beta Access Active",
|
|
115
|
+
border_style="magenta",
|
|
116
|
+
padding=(1, 2),
|
|
117
|
+
box=box.ROUNDED,
|
|
118
|
+
)
|
|
119
|
+
self.console.print(panel)
|
|
120
|
+
|
|
121
|
+
def _show_intro_panel(self):
|
|
122
|
+
message = (
|
|
123
|
+
"Warming up your research cockpitā¦\n"
|
|
124
|
+
"[dim]Loading config, telemetry, and background update checks.[/dim]"
|
|
125
|
+
)
|
|
126
|
+
panel = Panel(
|
|
127
|
+
message,
|
|
128
|
+
title="š Initializing Nocturnal Archive",
|
|
129
|
+
border_style="magenta",
|
|
130
|
+
padding=(1, 2),
|
|
131
|
+
box=box.ROUNDED,
|
|
132
|
+
)
|
|
133
|
+
self.console.print(panel)
|
|
134
|
+
|
|
135
|
+
def _show_ready_panel(self):
|
|
136
|
+
panel = Panel(
|
|
137
|
+
"Systems check complete.\n"
|
|
138
|
+
"Type [bold]help[/] for commands or [bold]tips[/] for power moves.",
|
|
139
|
+
title="ā
Nocturnal Archive ready!",
|
|
140
|
+
border_style="green",
|
|
141
|
+
padding=(1, 2),
|
|
142
|
+
box=box.ROUNDED,
|
|
143
|
+
)
|
|
144
|
+
self.console.print(panel)
|
|
145
|
+
|
|
146
|
+
def _enforce_latest_build(self):
|
|
147
|
+
"""Ensure the CLI is running the most recent published build."""
|
|
148
|
+
try:
|
|
149
|
+
updater = NocturnalUpdater()
|
|
150
|
+
update_info = updater.check_for_updates()
|
|
151
|
+
except Exception:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if not update_info or not update_info.get("available"):
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
latest_version = update_info.get("latest", "latest")
|
|
158
|
+
self.console.print(f"[banner]ā¬ļø Updating Nocturnal Archive to {latest_version} before launch...[/banner]")
|
|
159
|
+
|
|
160
|
+
if updater.update_package():
|
|
161
|
+
self._save_update_notification(latest_version)
|
|
162
|
+
self.console.print("[warning]ā»ļø Restarting to finish applying the update...[/warning]")
|
|
163
|
+
self._restart_cli()
|
|
164
|
+
|
|
165
|
+
def _restart_cli(self):
|
|
166
|
+
"""Re-exec the CLI using the current interpreter and arguments."""
|
|
167
|
+
try:
|
|
168
|
+
argv = [sys.executable, "-m", "nocturnal_archive.cli", *sys.argv[1:]]
|
|
169
|
+
os.execv(sys.executable, argv)
|
|
170
|
+
except Exception:
|
|
171
|
+
# If restart fails just continue in the current process.
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
def _save_update_notification(self, new_version):
|
|
175
|
+
"""Save update notification for next run"""
|
|
176
|
+
try:
|
|
177
|
+
import json
|
|
178
|
+
from pathlib import Path
|
|
179
|
+
|
|
180
|
+
notify_file = Path.home() / ".nocturnal_archive" / "update_notification.json"
|
|
181
|
+
notify_file.parent.mkdir(exist_ok=True)
|
|
182
|
+
|
|
183
|
+
with open(notify_file, 'w') as f:
|
|
184
|
+
json.dump({
|
|
185
|
+
"updated_to": new_version,
|
|
186
|
+
"timestamp": time.time()
|
|
187
|
+
}, f)
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
def _check_update_notification(self):
|
|
192
|
+
"""Check if we should show update notification"""
|
|
193
|
+
try:
|
|
194
|
+
import json
|
|
195
|
+
import time
|
|
196
|
+
from pathlib import Path
|
|
197
|
+
|
|
198
|
+
notify_file = Path.home() / ".nocturnal_archive" / "update_notification.json"
|
|
199
|
+
if notify_file.exists():
|
|
200
|
+
with open(notify_file, 'r') as f:
|
|
201
|
+
data = json.load(f)
|
|
202
|
+
|
|
203
|
+
# Show notification if update happened in last 24 hours
|
|
204
|
+
if time.time() - data.get("timestamp", 0) < 86400:
|
|
205
|
+
self.console.print(f"[success]š Updated to version {data['updated_to']}![/success]")
|
|
206
|
+
|
|
207
|
+
# Clean up notification
|
|
208
|
+
notify_file.unlink()
|
|
209
|
+
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
async def interactive_mode(self):
|
|
214
|
+
"""Interactive chat mode"""
|
|
215
|
+
if not await self.initialize():
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
self.console.print("\n[bold]š¤ Interactive Mode[/] ā Type your questions or 'quit' to exit")
|
|
219
|
+
self.console.rule(style="magenta")
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
while True:
|
|
223
|
+
try:
|
|
224
|
+
user_input = self.console.input("\n[bold cyan]š¤ You[/]: ").strip()
|
|
225
|
+
|
|
226
|
+
if user_input.lower() in ['quit', 'exit', 'q']:
|
|
227
|
+
break
|
|
228
|
+
if user_input.lower() == 'tips':
|
|
229
|
+
self.show_tips()
|
|
230
|
+
continue
|
|
231
|
+
if user_input.lower() == 'feedback':
|
|
232
|
+
self.collect_feedback()
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if not user_input:
|
|
236
|
+
continue
|
|
237
|
+
except (EOFError, KeyboardInterrupt):
|
|
238
|
+
self.console.print("\n[warning]š Goodbye![/warning]")
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
self.console.print("[bold violet]š¤ Agent[/]: ", end="", highlight=False)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
request = ChatRequest(
|
|
245
|
+
question=user_input,
|
|
246
|
+
user_id="cli_user",
|
|
247
|
+
conversation_id=self.session_id
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
response = await self.agent.process_request(request)
|
|
251
|
+
|
|
252
|
+
# Print response with proper formatting
|
|
253
|
+
self.console.print(response.response)
|
|
254
|
+
|
|
255
|
+
# Show usage stats occasionally
|
|
256
|
+
if hasattr(self.agent, 'daily_token_usage') and self.agent.daily_token_usage > 0:
|
|
257
|
+
stats = self.agent.get_usage_stats()
|
|
258
|
+
if stats['usage_percentage'] > 10: # Show if >10% used
|
|
259
|
+
self.console.print(f"\nš Usage: {stats['usage_percentage']:.1f}% of daily limit")
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
self.console.print(f"\n[error]ā Error: {e}[/error]")
|
|
263
|
+
|
|
264
|
+
finally:
|
|
265
|
+
if self.agent:
|
|
266
|
+
await self.agent.close()
|
|
267
|
+
|
|
268
|
+
async def single_query(self, question: str):
|
|
269
|
+
"""Process a single query"""
|
|
270
|
+
if not await self.initialize():
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
self.console.print(f"š¤ [bold]Processing[/]: {question}")
|
|
275
|
+
self.console.rule(style="magenta")
|
|
276
|
+
|
|
277
|
+
request = ChatRequest(
|
|
278
|
+
question=question,
|
|
279
|
+
user_id="cli_user",
|
|
280
|
+
conversation_id=self.session_id
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
response = await self.agent.process_request(request)
|
|
284
|
+
|
|
285
|
+
self.console.print(f"\nš [bold]Response[/]:\n{response.response}")
|
|
286
|
+
|
|
287
|
+
if response.tools_used:
|
|
288
|
+
self.console.print(f"\nš§ Tools used: {', '.join(response.tools_used)}")
|
|
289
|
+
|
|
290
|
+
if response.tokens_used > 0:
|
|
291
|
+
stats = self.agent.get_usage_stats()
|
|
292
|
+
self.console.print(
|
|
293
|
+
f"\nš Tokens used: {response.tokens_used} "
|
|
294
|
+
f"(Daily usage: {stats['usage_percentage']:.1f}%)"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
finally:
|
|
298
|
+
if self.agent:
|
|
299
|
+
await self.agent.close()
|
|
300
|
+
|
|
301
|
+
def setup_wizard(self):
|
|
302
|
+
"""Interactive setup wizard"""
|
|
303
|
+
config = NocturnalConfig()
|
|
304
|
+
return config.interactive_setup()
|
|
305
|
+
|
|
306
|
+
def show_tips(self):
|
|
307
|
+
"""Display a rotating set of CLI power tips"""
|
|
308
|
+
sample_count = 4 if len(self._tips) >= 4 else len(self._tips)
|
|
309
|
+
tips = random.sample(self._tips, sample_count)
|
|
310
|
+
table = Table(show_header=False, box=box.MINIMAL_DOUBLE_HEAD, padding=(0, 1))
|
|
311
|
+
for tip in tips:
|
|
312
|
+
table.add_row(f"⢠{tip}")
|
|
313
|
+
|
|
314
|
+
self.console.print(Panel(table, title="⨠Quick Tips", border_style="cyan", padding=(1, 2)))
|
|
315
|
+
self.console.print("[dim]Run `nocturnal tips` again for a fresh batch.[/dim]")
|
|
316
|
+
|
|
317
|
+
def collect_feedback(self) -> int:
|
|
318
|
+
"""Collect feedback from the user and store it locally"""
|
|
319
|
+
self.console.print(
|
|
320
|
+
Panel(
|
|
321
|
+
"Share whatās working, what feels rough, or any paper/finance workflows you wish existed.\n"
|
|
322
|
+
"Press Enter on an empty line to finish.",
|
|
323
|
+
title="š Beta Feedback",
|
|
324
|
+
border_style="cyan",
|
|
325
|
+
padding=(1, 2),
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
lines = []
|
|
330
|
+
while True:
|
|
331
|
+
try:
|
|
332
|
+
line = self.console.input("[dim]> [/]")
|
|
333
|
+
except (KeyboardInterrupt, EOFError):
|
|
334
|
+
self.console.print("[warning]Feedback capture cancelled.[/warning]")
|
|
335
|
+
return 1
|
|
336
|
+
|
|
337
|
+
if not line.strip():
|
|
338
|
+
break
|
|
339
|
+
lines.append(line)
|
|
340
|
+
|
|
341
|
+
if not lines:
|
|
342
|
+
self.console.print("[warning]No feedback captured ā nothing was saved.[/warning]")
|
|
343
|
+
return 1
|
|
344
|
+
|
|
345
|
+
feedback_dir = Path.home() / ".nocturnal_archive" / "feedback"
|
|
346
|
+
feedback_dir.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
|
|
348
|
+
feedback_path = feedback_dir / f"feedback-{timestamp}.md"
|
|
349
|
+
|
|
350
|
+
content = "\n".join(lines)
|
|
351
|
+
with open(feedback_path, "w", encoding="utf-8") as handle:
|
|
352
|
+
handle.write("# Nocturnal Archive Beta Feedback\n")
|
|
353
|
+
handle.write(f"timestamp = {timestamp}Z\n")
|
|
354
|
+
handle.write("\n")
|
|
355
|
+
handle.write(content)
|
|
356
|
+
handle.write("\n")
|
|
357
|
+
|
|
358
|
+
self.console.print(
|
|
359
|
+
f"[success]Thanks for the intel! Saved to[/success] [bold]{feedback_path}[/bold]"
|
|
360
|
+
)
|
|
361
|
+
self.console.print("[dim]Attach that file when you send feedback to the team.[/dim]")
|
|
362
|
+
return 0
|
|
363
|
+
|
|
364
|
+
def main():
|
|
365
|
+
"""Main CLI entry point"""
|
|
366
|
+
parser = argparse.ArgumentParser(
|
|
367
|
+
description="Nocturnal Archive - AI Research Assistant",
|
|
368
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
369
|
+
epilog="""
|
|
370
|
+
Examples:
|
|
371
|
+
nocturnal # Interactive mode
|
|
372
|
+
nocturnal "find papers on ML" # Single query
|
|
373
|
+
nocturnal --setup # Setup wizard
|
|
374
|
+
nocturnal --version # Show version
|
|
375
|
+
"""
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
parser.add_argument(
|
|
379
|
+
'query',
|
|
380
|
+
nargs='?',
|
|
381
|
+
help='Single query to process (if not provided, starts interactive mode)'
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
parser.add_argument(
|
|
385
|
+
'--setup',
|
|
386
|
+
action='store_true',
|
|
387
|
+
help='Run setup wizard for API keys'
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
parser.add_argument(
|
|
391
|
+
'--version',
|
|
392
|
+
action='store_true',
|
|
393
|
+
help='Show version information'
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
parser.add_argument(
|
|
397
|
+
'--interactive', '-i',
|
|
398
|
+
action='store_true',
|
|
399
|
+
help='Force interactive mode even with query'
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
parser.add_argument(
|
|
403
|
+
'--update',
|
|
404
|
+
action='store_true',
|
|
405
|
+
help='Check for and install updates'
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
parser.add_argument(
|
|
409
|
+
'--check-updates',
|
|
410
|
+
action='store_true',
|
|
411
|
+
help='Check for available updates'
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Auto-update is now enforced; no CLI flag provided to disable it.
|
|
415
|
+
|
|
416
|
+
parser.add_argument(
|
|
417
|
+
'--tips',
|
|
418
|
+
action='store_true',
|
|
419
|
+
help='Show quick CLI tips and exit'
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
parser.add_argument(
|
|
423
|
+
'--feedback',
|
|
424
|
+
action='store_true',
|
|
425
|
+
help='Capture beta feedback and save it locally'
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
parser.add_argument(
|
|
429
|
+
'--import-secrets',
|
|
430
|
+
metavar='PATH',
|
|
431
|
+
help='Import API keys from a .env style file'
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
parser.add_argument(
|
|
435
|
+
'--no-plaintext',
|
|
436
|
+
action='store_true',
|
|
437
|
+
help='Fail secret import if keyring is unavailable'
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
args = parser.parse_args()
|
|
441
|
+
|
|
442
|
+
# Handle version
|
|
443
|
+
if args.version:
|
|
444
|
+
print("Nocturnal Archive v1.0.0")
|
|
445
|
+
print("AI Research Assistant with real data integration")
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
if args.tips or (args.query and args.query.lower() == "tips" and not args.interactive):
|
|
449
|
+
cli = NocturnalCLI()
|
|
450
|
+
cli.show_tips()
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
if args.feedback or (args.query and args.query.lower() == "feedback" and not args.interactive):
|
|
454
|
+
cli = NocturnalCLI()
|
|
455
|
+
exit_code = cli.collect_feedback()
|
|
456
|
+
sys.exit(exit_code)
|
|
457
|
+
|
|
458
|
+
# Handle secret import before setup as it can be used non-interactively
|
|
459
|
+
if args.import_secrets:
|
|
460
|
+
config = NocturnalConfig()
|
|
461
|
+
try:
|
|
462
|
+
results = config.import_from_env_file(args.import_secrets, allow_plaintext=not args.no_plaintext)
|
|
463
|
+
except FileNotFoundError as exc:
|
|
464
|
+
print(f"ā {exc}")
|
|
465
|
+
sys.exit(1)
|
|
466
|
+
if not results:
|
|
467
|
+
print("ā ļø No supported secrets found in the provided file.")
|
|
468
|
+
sys.exit(1)
|
|
469
|
+
overall_success = True
|
|
470
|
+
for key, (status, message) in results.items():
|
|
471
|
+
label = MANAGED_SECRETS.get(key, {}).get('label', key)
|
|
472
|
+
icon = "ā
" if status else "ā ļø"
|
|
473
|
+
print(f"{icon} {label}: {message}")
|
|
474
|
+
if not status:
|
|
475
|
+
overall_success = False
|
|
476
|
+
sys.exit(0 if overall_success else 1)
|
|
477
|
+
|
|
478
|
+
# Handle setup
|
|
479
|
+
if args.setup:
|
|
480
|
+
cli = NocturnalCLI()
|
|
481
|
+
success = cli.setup_wizard()
|
|
482
|
+
sys.exit(0 if success else 1)
|
|
483
|
+
|
|
484
|
+
# Handle updates
|
|
485
|
+
if args.update or args.check_updates:
|
|
486
|
+
updater = NocturnalUpdater()
|
|
487
|
+
if args.update:
|
|
488
|
+
success = updater.update_package()
|
|
489
|
+
sys.exit(0 if success else 1)
|
|
490
|
+
else:
|
|
491
|
+
updater.show_update_status()
|
|
492
|
+
sys.exit(0)
|
|
493
|
+
|
|
494
|
+
# Handle query or interactive mode
|
|
495
|
+
async def run_cli():
|
|
496
|
+
cli = NocturnalCLI()
|
|
497
|
+
|
|
498
|
+
if args.query and not args.interactive:
|
|
499
|
+
await cli.single_query(args.query)
|
|
500
|
+
else:
|
|
501
|
+
await cli.interactive_mode()
|
|
502
|
+
|
|
503
|
+
try:
|
|
504
|
+
asyncio.run(run_cli())
|
|
505
|
+
except KeyboardInterrupt:
|
|
506
|
+
print("\nš Goodbye!")
|
|
507
|
+
except Exception as e:
|
|
508
|
+
print(f"ā Error: {e}")
|
|
509
|
+
sys.exit(1)
|
|
510
|
+
|
|
511
|
+
if __name__ == "__main__":
|
|
512
|
+
main()
|