nc1709 1.15.4__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.
Files changed (86) hide show
  1. nc1709/__init__.py +13 -0
  2. nc1709/agent/__init__.py +36 -0
  3. nc1709/agent/core.py +505 -0
  4. nc1709/agent/mcp_bridge.py +245 -0
  5. nc1709/agent/permissions.py +298 -0
  6. nc1709/agent/tools/__init__.py +21 -0
  7. nc1709/agent/tools/base.py +440 -0
  8. nc1709/agent/tools/bash_tool.py +367 -0
  9. nc1709/agent/tools/file_tools.py +454 -0
  10. nc1709/agent/tools/notebook_tools.py +516 -0
  11. nc1709/agent/tools/search_tools.py +322 -0
  12. nc1709/agent/tools/task_tool.py +284 -0
  13. nc1709/agent/tools/web_tools.py +555 -0
  14. nc1709/agents/__init__.py +17 -0
  15. nc1709/agents/auto_fix.py +506 -0
  16. nc1709/agents/test_generator.py +507 -0
  17. nc1709/checkpoints.py +372 -0
  18. nc1709/cli.py +3380 -0
  19. nc1709/cli_ui.py +1080 -0
  20. nc1709/cognitive/__init__.py +149 -0
  21. nc1709/cognitive/anticipation.py +594 -0
  22. nc1709/cognitive/context_engine.py +1046 -0
  23. nc1709/cognitive/council.py +824 -0
  24. nc1709/cognitive/learning.py +761 -0
  25. nc1709/cognitive/router.py +583 -0
  26. nc1709/cognitive/system.py +519 -0
  27. nc1709/config.py +155 -0
  28. nc1709/custom_commands.py +300 -0
  29. nc1709/executor.py +333 -0
  30. nc1709/file_controller.py +354 -0
  31. nc1709/git_integration.py +308 -0
  32. nc1709/github_integration.py +477 -0
  33. nc1709/image_input.py +446 -0
  34. nc1709/linting.py +519 -0
  35. nc1709/llm_adapter.py +667 -0
  36. nc1709/logger.py +192 -0
  37. nc1709/mcp/__init__.py +18 -0
  38. nc1709/mcp/client.py +370 -0
  39. nc1709/mcp/manager.py +407 -0
  40. nc1709/mcp/protocol.py +210 -0
  41. nc1709/mcp/server.py +473 -0
  42. nc1709/memory/__init__.py +20 -0
  43. nc1709/memory/embeddings.py +325 -0
  44. nc1709/memory/indexer.py +474 -0
  45. nc1709/memory/sessions.py +432 -0
  46. nc1709/memory/vector_store.py +451 -0
  47. nc1709/models/__init__.py +86 -0
  48. nc1709/models/detector.py +377 -0
  49. nc1709/models/formats.py +315 -0
  50. nc1709/models/manager.py +438 -0
  51. nc1709/models/registry.py +497 -0
  52. nc1709/performance/__init__.py +343 -0
  53. nc1709/performance/cache.py +705 -0
  54. nc1709/performance/pipeline.py +611 -0
  55. nc1709/performance/tiering.py +543 -0
  56. nc1709/plan_mode.py +362 -0
  57. nc1709/plugins/__init__.py +17 -0
  58. nc1709/plugins/agents/__init__.py +18 -0
  59. nc1709/plugins/agents/django_agent.py +912 -0
  60. nc1709/plugins/agents/docker_agent.py +623 -0
  61. nc1709/plugins/agents/fastapi_agent.py +887 -0
  62. nc1709/plugins/agents/git_agent.py +731 -0
  63. nc1709/plugins/agents/nextjs_agent.py +867 -0
  64. nc1709/plugins/base.py +359 -0
  65. nc1709/plugins/manager.py +411 -0
  66. nc1709/plugins/registry.py +337 -0
  67. nc1709/progress.py +443 -0
  68. nc1709/prompts/__init__.py +22 -0
  69. nc1709/prompts/agent_system.py +180 -0
  70. nc1709/prompts/task_prompts.py +340 -0
  71. nc1709/prompts/unified_prompt.py +133 -0
  72. nc1709/reasoning_engine.py +541 -0
  73. nc1709/remote_client.py +266 -0
  74. nc1709/shell_completions.py +349 -0
  75. nc1709/slash_commands.py +649 -0
  76. nc1709/task_classifier.py +408 -0
  77. nc1709/version_check.py +177 -0
  78. nc1709/web/__init__.py +8 -0
  79. nc1709/web/server.py +950 -0
  80. nc1709/web/templates/index.html +1127 -0
  81. nc1709-1.15.4.dist-info/METADATA +858 -0
  82. nc1709-1.15.4.dist-info/RECORD +86 -0
  83. nc1709-1.15.4.dist-info/WHEEL +5 -0
  84. nc1709-1.15.4.dist-info/entry_points.txt +2 -0
  85. nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
  86. nc1709-1.15.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,408 @@
1
+ """
2
+ LLM-Based Task Classifier
3
+ Uses a small, fast LLM call to intelligently classify user requests
4
+ """
5
+ import json
6
+ import re
7
+ from typing import Optional, Dict, Any, List, Tuple
8
+ from enum import Enum
9
+
10
+
11
+ class TaskCategory(Enum):
12
+ """Categories for task classification"""
13
+ CODE_GENERATION = "code_generation" # Write new code
14
+ CODE_EXPLANATION = "code_explanation" # Explain existing code
15
+ CODE_REFACTORING = "code_refactoring" # Improve/refactor code
16
+ CODE_DEBUGGING = "code_debugging" # Fix bugs/errors
17
+ FILE_OPERATIONS = "file_operations" # Read/write/modify files
18
+ SHELL_COMMANDS = "shell_commands" # Execute terminal commands
19
+ GIT_OPERATIONS = "git_operations" # Git-related tasks
20
+ DOCKER_OPERATIONS = "docker_operations" # Docker-related tasks
21
+ PROJECT_SETUP = "project_setup" # Create/scaffold projects
22
+ DOCUMENTATION = "documentation" # Write docs, comments
23
+ TESTING = "testing" # Generate/run tests
24
+ ARCHITECTURE = "architecture" # Design, planning
25
+ GENERAL_QA = "general_qa" # General questions
26
+ QUICK_ANSWER = "quick_answer" # Simple, fast responses
27
+
28
+
29
+ class TaskComplexity(Enum):
30
+ """Complexity levels for tasks"""
31
+ TRIVIAL = "trivial" # Single-step, instant
32
+ SIMPLE = "simple" # Few steps, quick
33
+ MODERATE = "moderate" # Multiple steps
34
+ COMPLEX = "complex" # Many steps, planning needed
35
+ EXPERT = "expert" # Requires deep analysis
36
+
37
+
38
+ class ClassificationResult:
39
+ """Result of task classification"""
40
+
41
+ def __init__(
42
+ self,
43
+ category: TaskCategory,
44
+ complexity: TaskComplexity,
45
+ confidence: float,
46
+ suggested_model: str,
47
+ requires_tools: bool,
48
+ estimated_steps: int,
49
+ reasoning: str
50
+ ):
51
+ self.category = category
52
+ self.complexity = complexity
53
+ self.confidence = confidence
54
+ self.suggested_model = suggested_model
55
+ self.requires_tools = requires_tools
56
+ self.estimated_steps = estimated_steps
57
+ self.reasoning = reasoning
58
+
59
+ def to_dict(self) -> Dict[str, Any]:
60
+ return {
61
+ "category": self.category.value,
62
+ "complexity": self.complexity.value,
63
+ "confidence": self.confidence,
64
+ "suggested_model": self.suggested_model,
65
+ "requires_tools": self.requires_tools,
66
+ "estimated_steps": self.estimated_steps,
67
+ "reasoning": self.reasoning
68
+ }
69
+
70
+
71
+ class SmartTaskClassifier:
72
+ """LLM-powered task classifier with fallback to keyword matching"""
73
+
74
+ # Fast classification prompt
75
+ CLASSIFICATION_PROMPT = """Classify this task. Respond with ONLY a JSON object, no other text.
76
+
77
+ Task: {task}
78
+
79
+ JSON format:
80
+ {{
81
+ "category": "code_generation|code_explanation|code_refactoring|code_debugging|file_operations|shell_commands|git_operations|docker_operations|project_setup|documentation|testing|architecture|general_qa|quick_answer",
82
+ "complexity": "trivial|simple|moderate|complex|expert",
83
+ "confidence": 0.0-1.0,
84
+ "requires_tools": true|false,
85
+ "steps": 1-10,
86
+ "reason": "brief explanation"
87
+ }}"""
88
+
89
+ # Keyword patterns for fast fallback classification
90
+ PATTERNS = {
91
+ TaskCategory.CODE_GENERATION: [
92
+ r"\b(write|create|generate|implement|build|make)\b.*\b(function|class|code|script|program|api|module)\b",
93
+ r"\b(code|function|class|script)\b.*\b(for|that|to|which)\b"
94
+ ],
95
+ TaskCategory.CODE_EXPLANATION: [
96
+ r"\b(explain|what does|how does|understand|clarify)\b.*\b(code|function|this|it)\b",
97
+ r"\bwhat\s+is\b.*\b(doing|for|purpose)\b"
98
+ ],
99
+ TaskCategory.CODE_REFACTORING: [
100
+ r"\b(refactor|improve|optimize|clean up|simplify|rewrite)\b",
101
+ r"\bmake\b.*\b(better|cleaner|faster|readable)\b"
102
+ ],
103
+ TaskCategory.CODE_DEBUGGING: [
104
+ r"\b(fix|debug|error|bug|issue|problem|broken|not working)\b",
105
+ r"\bwhy\b.*\b(fail|error|crash|wrong)\b"
106
+ ],
107
+ TaskCategory.FILE_OPERATIONS: [
108
+ r"\b(read|write|create|delete|modify|edit|save)\b.*\b(file|directory|folder)\b",
109
+ r"\bfile\s+(content|path|name)\b"
110
+ ],
111
+ TaskCategory.SHELL_COMMANDS: [
112
+ r"\b(run|execute|command|terminal|shell|bash)\b",
113
+ r"\b(npm|pip|yarn|cargo|make|apt|brew)\b"
114
+ ],
115
+ TaskCategory.GIT_OPERATIONS: [
116
+ r"\b(git|commit|push|pull|branch|merge|clone|diff|status)\b"
117
+ ],
118
+ TaskCategory.DOCKER_OPERATIONS: [
119
+ r"\b(docker|container|image|compose|kubernetes|k8s)\b"
120
+ ],
121
+ TaskCategory.PROJECT_SETUP: [
122
+ r"\b(setup|scaffold|initialize|bootstrap|create project|new project)\b",
123
+ r"\b(fastapi|django|flask|next\.?js|react|vue)\b.*\b(project|app)\b"
124
+ ],
125
+ TaskCategory.DOCUMENTATION: [
126
+ r"\b(document|docstring|readme|comment|annotate)\b",
127
+ r"\badd\b.*\b(docs|documentation|comments)\b"
128
+ ],
129
+ TaskCategory.TESTING: [
130
+ r"\b(test|unittest|pytest|jest|spec|coverage)\b",
131
+ r"\bgenerate\b.*\btests?\b"
132
+ ],
133
+ TaskCategory.ARCHITECTURE: [
134
+ r"\b(design|architect|plan|structure|organize)\b",
135
+ r"\bhow\s+should\s+i\b"
136
+ ],
137
+ TaskCategory.QUICK_ANSWER: [
138
+ r"^(what is|who is|when|where|how many|yes or no)\b",
139
+ r"^.{0,30}\?$" # Short questions
140
+ ]
141
+ }
142
+
143
+ # Complexity indicators
144
+ COMPLEXITY_PATTERNS = {
145
+ TaskComplexity.TRIVIAL: [
146
+ r"^(what is|define|list)\b",
147
+ r"\bjust\b",
148
+ r"\bsimple\b"
149
+ ],
150
+ TaskComplexity.SIMPLE: [
151
+ r"\b(quick|brief|short)\b",
152
+ r"\bone\b.*\b(function|file|class)\b"
153
+ ],
154
+ TaskComplexity.MODERATE: [
155
+ r"\b(several|few|some)\b",
156
+ r"\band\b.*\band\b" # Multiple requirements
157
+ ],
158
+ TaskComplexity.COMPLEX: [
159
+ r"\b(entire|whole|full|complete)\b.*\b(app|application|system)\b",
160
+ r"\brefactor\b.*\b(entire|whole|all)\b"
161
+ ],
162
+ TaskComplexity.EXPERT: [
163
+ r"\b(migrate|architecture|redesign|scale)\b",
164
+ r"\bfrom scratch\b"
165
+ ]
166
+ }
167
+
168
+ # Model recommendations by category and complexity
169
+ MODEL_RECOMMENDATIONS = {
170
+ (TaskCategory.QUICK_ANSWER, TaskComplexity.TRIVIAL): "fast",
171
+ (TaskCategory.QUICK_ANSWER, TaskComplexity.SIMPLE): "fast",
172
+ (TaskCategory.CODE_GENERATION, TaskComplexity.TRIVIAL): "fast",
173
+ (TaskCategory.CODE_GENERATION, TaskComplexity.SIMPLE): "coding",
174
+ (TaskCategory.CODE_GENERATION, TaskComplexity.MODERATE): "coding",
175
+ (TaskCategory.CODE_GENERATION, TaskComplexity.COMPLEX): "coding",
176
+ (TaskCategory.CODE_GENERATION, TaskComplexity.EXPERT): "reasoning",
177
+ (TaskCategory.ARCHITECTURE, TaskComplexity.MODERATE): "reasoning",
178
+ (TaskCategory.ARCHITECTURE, TaskComplexity.COMPLEX): "reasoning",
179
+ (TaskCategory.ARCHITECTURE, TaskComplexity.EXPERT): "reasoning",
180
+ }
181
+
182
+ def __init__(self, llm_adapter=None):
183
+ """Initialize classifier with optional LLM for smart classification
184
+
185
+ Args:
186
+ llm_adapter: LLMAdapter instance for LLM-based classification
187
+ """
188
+ self.llm = llm_adapter
189
+ self._use_llm = llm_adapter is not None
190
+
191
+ def classify(
192
+ self,
193
+ task: str,
194
+ context: Optional[Dict[str, Any]] = None,
195
+ use_llm: bool = True
196
+ ) -> ClassificationResult:
197
+ """Classify a task using LLM or keyword matching
198
+
199
+ Args:
200
+ task: User's task/request
201
+ context: Additional context (file paths, history, etc.)
202
+ use_llm: Whether to attempt LLM classification
203
+
204
+ Returns:
205
+ ClassificationResult with category, complexity, and recommendations
206
+ """
207
+ # Try LLM classification first if available and enabled
208
+ if use_llm and self._use_llm and self.llm:
209
+ try:
210
+ result = self._classify_with_llm(task)
211
+ if result and result.confidence >= 0.7:
212
+ return result
213
+ except Exception:
214
+ pass # Fall back to keyword matching
215
+
216
+ # Fall back to keyword matching
217
+ return self._classify_with_keywords(task, context)
218
+
219
+ def _classify_with_llm(self, task: str) -> Optional[ClassificationResult]:
220
+ """Classify task using LLM
221
+
222
+ Args:
223
+ task: User's task
224
+
225
+ Returns:
226
+ ClassificationResult or None if classification fails
227
+ """
228
+ from .llm_adapter import TaskType
229
+
230
+ prompt = self.CLASSIFICATION_PROMPT.format(task=task)
231
+
232
+ # Use fast model for classification
233
+ response = self.llm.complete(prompt, task_type=TaskType.FAST, max_tokens=200)
234
+
235
+ # Parse JSON response
236
+ try:
237
+ # Extract JSON from response
238
+ json_match = re.search(r'\{[^{}]+\}', response, re.DOTALL)
239
+ if not json_match:
240
+ return None
241
+
242
+ data = json.loads(json_match.group())
243
+
244
+ category = TaskCategory(data.get("category", "general_qa"))
245
+ complexity = TaskComplexity(data.get("complexity", "moderate"))
246
+ confidence = float(data.get("confidence", 0.8))
247
+ requires_tools = bool(data.get("requires_tools", False))
248
+ steps = int(data.get("steps", 1))
249
+ reason = data.get("reason", "LLM classification")
250
+
251
+ # Get model recommendation
252
+ suggested_model = self._get_model_recommendation(category, complexity)
253
+
254
+ return ClassificationResult(
255
+ category=category,
256
+ complexity=complexity,
257
+ confidence=confidence,
258
+ suggested_model=suggested_model,
259
+ requires_tools=requires_tools,
260
+ estimated_steps=steps,
261
+ reasoning=reason
262
+ )
263
+
264
+ except (json.JSONDecodeError, ValueError, KeyError):
265
+ return None
266
+
267
+ def _classify_with_keywords(
268
+ self,
269
+ task: str,
270
+ context: Optional[Dict[str, Any]] = None
271
+ ) -> ClassificationResult:
272
+ """Classify task using keyword patterns
273
+
274
+ Args:
275
+ task: User's task
276
+ context: Additional context
277
+
278
+ Returns:
279
+ ClassificationResult
280
+ """
281
+ task_lower = task.lower()
282
+
283
+ # Find best matching category
284
+ best_category = TaskCategory.GENERAL_QA
285
+ best_score = 0
286
+
287
+ for category, patterns in self.PATTERNS.items():
288
+ score = 0
289
+ for pattern in patterns:
290
+ if re.search(pattern, task_lower, re.IGNORECASE):
291
+ score += 1
292
+
293
+ if score > best_score:
294
+ best_score = score
295
+ best_category = category
296
+
297
+ # Determine complexity
298
+ complexity = TaskComplexity.MODERATE
299
+ for comp_level, patterns in self.COMPLEXITY_PATTERNS.items():
300
+ for pattern in patterns:
301
+ if re.search(pattern, task_lower, re.IGNORECASE):
302
+ complexity = comp_level
303
+ break
304
+
305
+ # Estimate based on task length and keywords
306
+ word_count = len(task.split())
307
+ if word_count < 10:
308
+ complexity = TaskComplexity.SIMPLE
309
+ elif word_count > 50:
310
+ complexity = TaskComplexity.COMPLEX
311
+
312
+ # Determine if tools are required
313
+ requires_tools = best_category in [
314
+ TaskCategory.FILE_OPERATIONS,
315
+ TaskCategory.SHELL_COMMANDS,
316
+ TaskCategory.GIT_OPERATIONS,
317
+ TaskCategory.DOCKER_OPERATIONS,
318
+ TaskCategory.PROJECT_SETUP,
319
+ TaskCategory.TESTING
320
+ ]
321
+
322
+ # Estimate steps
323
+ steps_map = {
324
+ TaskComplexity.TRIVIAL: 1,
325
+ TaskComplexity.SIMPLE: 2,
326
+ TaskComplexity.MODERATE: 4,
327
+ TaskComplexity.COMPLEX: 7,
328
+ TaskComplexity.EXPERT: 10
329
+ }
330
+ estimated_steps = steps_map.get(complexity, 3)
331
+
332
+ # Get model recommendation
333
+ suggested_model = self._get_model_recommendation(best_category, complexity)
334
+
335
+ # Calculate confidence based on match score
336
+ confidence = min(0.5 + (best_score * 0.15), 0.95)
337
+
338
+ return ClassificationResult(
339
+ category=best_category,
340
+ complexity=complexity,
341
+ confidence=confidence,
342
+ suggested_model=suggested_model,
343
+ requires_tools=requires_tools,
344
+ estimated_steps=estimated_steps,
345
+ reasoning=f"Keyword match (score: {best_score})"
346
+ )
347
+
348
+ def _get_model_recommendation(
349
+ self,
350
+ category: TaskCategory,
351
+ complexity: TaskComplexity
352
+ ) -> str:
353
+ """Get recommended model for task
354
+
355
+ Args:
356
+ category: Task category
357
+ complexity: Task complexity
358
+
359
+ Returns:
360
+ Model name (fast, coding, reasoning, general)
361
+ """
362
+ # Check specific recommendations
363
+ key = (category, complexity)
364
+ if key in self.MODEL_RECOMMENDATIONS:
365
+ return self.MODEL_RECOMMENDATIONS[key]
366
+
367
+ # Default recommendations by category
368
+ category_defaults = {
369
+ TaskCategory.CODE_GENERATION: "coding",
370
+ TaskCategory.CODE_EXPLANATION: "general",
371
+ TaskCategory.CODE_REFACTORING: "coding",
372
+ TaskCategory.CODE_DEBUGGING: "coding",
373
+ TaskCategory.FILE_OPERATIONS: "tools",
374
+ TaskCategory.SHELL_COMMANDS: "tools",
375
+ TaskCategory.GIT_OPERATIONS: "tools",
376
+ TaskCategory.DOCKER_OPERATIONS: "tools",
377
+ TaskCategory.PROJECT_SETUP: "coding",
378
+ TaskCategory.DOCUMENTATION: "general",
379
+ TaskCategory.TESTING: "coding",
380
+ TaskCategory.ARCHITECTURE: "reasoning",
381
+ TaskCategory.GENERAL_QA: "general",
382
+ TaskCategory.QUICK_ANSWER: "fast"
383
+ }
384
+
385
+ return category_defaults.get(category, "general")
386
+
387
+ def get_task_summary(self, task: str) -> str:
388
+ """Get a brief summary of task classification
389
+
390
+ Args:
391
+ task: User's task
392
+
393
+ Returns:
394
+ Human-readable summary
395
+ """
396
+ result = self.classify(task, use_llm=False) # Fast keyword classification
397
+
398
+ complexity_icons = {
399
+ TaskComplexity.TRIVIAL: "⚡",
400
+ TaskComplexity.SIMPLE: "🟢",
401
+ TaskComplexity.MODERATE: "🟡",
402
+ TaskComplexity.COMPLEX: "🟠",
403
+ TaskComplexity.EXPERT: "🔴"
404
+ }
405
+
406
+ icon = complexity_icons.get(result.complexity, "⚪")
407
+
408
+ return f"{icon} {result.category.value.replace('_', ' ').title()} ({result.complexity.value})"
@@ -0,0 +1,177 @@
1
+ """
2
+ Version Check - Automatic update notification for NC1709
3
+
4
+ Checks PyPI for newer versions and notifies users on startup.
5
+ Caches the check result to avoid repeated API calls.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import time
11
+ from pathlib import Path
12
+ from typing import Optional, Tuple
13
+ from packaging import version
14
+
15
+ # Cache settings
16
+ CACHE_DIR = Path.home() / ".nc1709"
17
+ CACHE_FILE = CACHE_DIR / "version_cache.json"
18
+ CACHE_TTL = 86400 # Check once per day (24 hours in seconds)
19
+
20
+
21
+ def get_current_version() -> str:
22
+ """Get the currently installed version."""
23
+ from . import __version__
24
+ return __version__
25
+
26
+
27
+ def get_latest_version_from_pypi() -> Optional[str]:
28
+ """Fetch the latest version from PyPI.
29
+
30
+ Returns:
31
+ Latest version string or None if fetch fails
32
+ """
33
+ try:
34
+ import urllib.request
35
+ import urllib.error
36
+
37
+ url = "https://pypi.org/pypi/nc1709/json"
38
+
39
+ # Set a short timeout to not delay startup
40
+ request = urllib.request.Request(
41
+ url,
42
+ headers={"Accept": "application/json"}
43
+ )
44
+
45
+ with urllib.request.urlopen(request, timeout=3) as response:
46
+ data = json.loads(response.read().decode())
47
+ return data.get("info", {}).get("version")
48
+ except Exception:
49
+ # Silently fail - don't interrupt the user experience
50
+ return None
51
+
52
+
53
+ def load_cache() -> dict:
54
+ """Load the version check cache."""
55
+ try:
56
+ if CACHE_FILE.exists():
57
+ with open(CACHE_FILE, "r") as f:
58
+ return json.load(f)
59
+ except Exception:
60
+ pass
61
+ return {}
62
+
63
+
64
+ def save_cache(cache: dict) -> None:
65
+ """Save the version check cache."""
66
+ try:
67
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
68
+ with open(CACHE_FILE, "w") as f:
69
+ json.dump(cache, f)
70
+ except Exception:
71
+ pass
72
+
73
+
74
+ def check_for_update(force: bool = False) -> Tuple[bool, Optional[str], Optional[str]]:
75
+ """Check if a newer version is available.
76
+
77
+ Args:
78
+ force: If True, bypass cache and check PyPI directly
79
+
80
+ Returns:
81
+ Tuple of (update_available, current_version, latest_version)
82
+ """
83
+ current = get_current_version()
84
+
85
+ # Check cache first (unless forced)
86
+ if not force:
87
+ cache = load_cache()
88
+ cache_time = cache.get("timestamp", 0)
89
+
90
+ if time.time() - cache_time < CACHE_TTL:
91
+ # Use cached result
92
+ latest = cache.get("latest_version")
93
+ if latest:
94
+ try:
95
+ update_available = version.parse(latest) > version.parse(current)
96
+ return (update_available, current, latest)
97
+ except Exception:
98
+ pass
99
+
100
+ # Fetch from PyPI
101
+ latest = get_latest_version_from_pypi()
102
+
103
+ if latest:
104
+ # Update cache
105
+ save_cache({
106
+ "timestamp": time.time(),
107
+ "latest_version": latest,
108
+ "current_version": current
109
+ })
110
+
111
+ try:
112
+ update_available = version.parse(latest) > version.parse(current)
113
+ return (update_available, current, latest)
114
+ except Exception:
115
+ return (False, current, latest)
116
+
117
+ return (False, current, None)
118
+
119
+
120
+ def get_update_message(current: str, latest: str) -> str:
121
+ """Generate a user-friendly update message.
122
+
123
+ Args:
124
+ current: Current installed version
125
+ latest: Latest available version
126
+
127
+ Returns:
128
+ Formatted update message
129
+ """
130
+ return f"""
131
+ ╭─────────────────────────────────────────────────────────────╮
132
+ │ 🆕 NC1709 Update Available! │
133
+ │ │
134
+ │ Current version: {current:<10} │
135
+ │ Latest version: {latest:<10} │
136
+ │ │
137
+ │ Run to update: pip install --upgrade nc1709 │
138
+ ╰─────────────────────────────────────────────────────────────╯
139
+ """
140
+
141
+
142
+ def check_and_notify(quiet: bool = False) -> Optional[str]:
143
+ """Check for updates and return notification message if available.
144
+
145
+ This is the main function to call on CLI startup.
146
+
147
+ Args:
148
+ quiet: If True, return None even if update available
149
+
150
+ Returns:
151
+ Update message string if update available, None otherwise
152
+ """
153
+ if quiet:
154
+ return None
155
+
156
+ try:
157
+ update_available, current, latest = check_for_update()
158
+
159
+ if update_available and current and latest:
160
+ return get_update_message(current, latest)
161
+ except Exception:
162
+ # Never crash the CLI due to version check
163
+ pass
164
+
165
+ return None
166
+
167
+
168
+ def print_update_notification() -> None:
169
+ """Print update notification if available.
170
+
171
+ Call this at CLI startup for automatic notifications.
172
+ """
173
+ from .cli_ui import Color
174
+
175
+ message = check_and_notify()
176
+ if message:
177
+ print(f"{Color.YELLOW}{message}{Color.RESET}")
nc1709/web/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ NC1709 Web Dashboard
3
+ Local web interface for NC1709 AI assistant
4
+ """
5
+
6
+ from .server import create_app, run_server
7
+
8
+ __all__ = ["create_app", "run_server"]