cct-cli 0.7.9.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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CCT — package_research.py: Smart Package Research (v0.7.7 spec section 2).
|
|
3
|
+
|
|
4
|
+
When the user asks to install a package the local tables in packages.py
|
|
5
|
+
don't recognize, the AI is supposed to do its homework before touching
|
|
6
|
+
the system:
|
|
7
|
+
|
|
8
|
+
* search documentation / read official docs
|
|
9
|
+
* compare alternatives
|
|
10
|
+
* check compatibility (platform, python version, etc.)
|
|
11
|
+
* show: popularity, latest version, license, platform support,
|
|
12
|
+
maintenance status, breaking changes, security warnings
|
|
13
|
+
|
|
14
|
+
and only install after the user approves.
|
|
15
|
+
|
|
16
|
+
How this is implemented, honestly:
|
|
17
|
+
|
|
18
|
+
* For Python packages this queries the real PyPI JSON API
|
|
19
|
+
(https://pypi.org/pypi/<name>/json) when `requests` is available —
|
|
20
|
+
latest version, license, release history (maintenance freshness),
|
|
21
|
+
and the current release's requires-python (compatibility) all come
|
|
22
|
+
straight from the registry, not from guesswork.
|
|
23
|
+
* For npm packages it queries the npm registry
|
|
24
|
+
(https://registry.npmjs.org/<name>) the same way.
|
|
25
|
+
* Anything else (or when the registry APIs are unreachable) falls
|
|
26
|
+
back to the app's existing web_search (aicore.web_search), and the
|
|
27
|
+
card marks which fields are *sourced* vs *estimated*. The module
|
|
28
|
+
never fabricates version numbers or licenses.
|
|
29
|
+
* "Security warnings" and "breaking changes" are NOT auto-detected
|
|
30
|
+
from arbitrary web pages (that would mean trusting scraper guesses
|
|
31
|
+
as fact) — the card reports them when a registry/API actually
|
|
32
|
+
provides them (e.g. PyPI's 'yanked' releases, vulnerability info we
|
|
33
|
+
can parse), and otherwise points the user at the package's own
|
|
34
|
+
changelog/advisory page as a human-check step.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
print("This is a library file and is not meant to be run directly.")
|
|
39
|
+
import sys
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
import json
|
|
43
|
+
import re
|
|
44
|
+
import time
|
|
45
|
+
|
|
46
|
+
from . import packages
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
import requests
|
|
50
|
+
_HAS_REQUESTS = True
|
|
51
|
+
except ImportError:
|
|
52
|
+
requests = None
|
|
53
|
+
_HAS_REQUESTS = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# --------------------------------------------------------------- PyPI --
|
|
57
|
+
def _pypi_info(name):
|
|
58
|
+
"""Real PyPI registry lookup. Returns a dict or None on any
|
|
59
|
+
failure (offline, unknown package, no requests)."""
|
|
60
|
+
if not _HAS_REQUESTS:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
resp = requests.get(f"https://pypi.org/pypi/{name}/json",
|
|
64
|
+
timeout=15, headers={"User-Agent": "CCT-package-research/0.7.7"})
|
|
65
|
+
if resp.status_code != 200:
|
|
66
|
+
return None
|
|
67
|
+
data = resp.json()
|
|
68
|
+
info = data.get("info", {})
|
|
69
|
+
releases = data.get("releases", {}) or {}
|
|
70
|
+
versions = [v for v, files in releases.items() if files]
|
|
71
|
+
rel_dates = []
|
|
72
|
+
for v, files in releases.items():
|
|
73
|
+
for f in (files or [])[:1]:
|
|
74
|
+
if f.get("upload_time"):
|
|
75
|
+
rel_dates.append((f["upload_time"], v))
|
|
76
|
+
rel_dates.sort()
|
|
77
|
+
latest = info.get("version", "unknown")
|
|
78
|
+
return {
|
|
79
|
+
"name": info.get("name", name),
|
|
80
|
+
"latest_version": latest,
|
|
81
|
+
"summary": info.get("summary") or "",
|
|
82
|
+
"license": info.get("license") or info.get("license_expression") or "unspecified",
|
|
83
|
+
"requires_python": info.get("requires_python") or "any",
|
|
84
|
+
"home_page": info.get("home_page") or info.get("project_url") or "",
|
|
85
|
+
"project_urls": info.get("project_urls") or {},
|
|
86
|
+
"total_versions": len(versions),
|
|
87
|
+
"first_release": rel_dates[0][0][:10] if rel_dates else None,
|
|
88
|
+
"last_release": rel_dates[-1][0][:10] if rel_dates else None,
|
|
89
|
+
"source": "pypi",
|
|
90
|
+
}
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _npm_info(name):
|
|
96
|
+
"""Real npm registry lookup (dist-tags + latest version metadata)."""
|
|
97
|
+
if not _HAS_REQUESTS:
|
|
98
|
+
return None
|
|
99
|
+
try:
|
|
100
|
+
resp = requests.get(f"https://registry.npmjs.org/{name}",
|
|
101
|
+
timeout=15, headers={"User-Agent": "CCT-package-research/0.7.7"})
|
|
102
|
+
if resp.status_code != 200:
|
|
103
|
+
return None
|
|
104
|
+
data = resp.json()
|
|
105
|
+
dist = data.get("dist-tags", {})
|
|
106
|
+
latest = dist.get("latest", "unknown")
|
|
107
|
+
info = data.get("versions", {}).get(latest, {}) or {}
|
|
108
|
+
time_map = data.get("time", {}) or {}
|
|
109
|
+
return {
|
|
110
|
+
"name": data.get("name", name),
|
|
111
|
+
"latest_version": latest,
|
|
112
|
+
"summary": info.get("description") or "",
|
|
113
|
+
"license": (info.get("license") or {}).get("type") if isinstance(
|
|
114
|
+
info.get("license"), dict) else str(info.get("license") or "unspecified"),
|
|
115
|
+
"requires_python": None,
|
|
116
|
+
"home_page": info.get("homepage") or "",
|
|
117
|
+
"project_urls": {},
|
|
118
|
+
"total_versions": len(data.get("versions", {}) or {}),
|
|
119
|
+
"first_release": str(time_map.get("created", ""))[:10] or None,
|
|
120
|
+
"last_release": str(time_map.get(latest, ""))[:10] or None,
|
|
121
|
+
"source": "npm",
|
|
122
|
+
}
|
|
123
|
+
except Exception:
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------- web fallback --
|
|
128
|
+
def _web_info(name):
|
|
129
|
+
"""Fallback research via aicore.web_search — the same live-search
|
|
130
|
+
path /research already uses. Returns a dict with sourced/estimated
|
|
131
|
+
fields, or None if the search is unreachable."""
|
|
132
|
+
try:
|
|
133
|
+
from . import aicore
|
|
134
|
+
results = aicore.web_search(
|
|
135
|
+
f"package library {name} latest version license alternatives", max_results=5)
|
|
136
|
+
except Exception:
|
|
137
|
+
return None
|
|
138
|
+
if not results:
|
|
139
|
+
return None
|
|
140
|
+
snippets = " ".join(r.get("snippet", "") for r in results[:4])
|
|
141
|
+
info = {
|
|
142
|
+
"name": name,
|
|
143
|
+
"latest_version": None,
|
|
144
|
+
"summary": results[0].get("snippet", "")[:300],
|
|
145
|
+
"license": None,
|
|
146
|
+
"requires_python": None,
|
|
147
|
+
"home_page": results[0].get("url", ""),
|
|
148
|
+
"project_urls": {},
|
|
149
|
+
"total_versions": None,
|
|
150
|
+
"first_release": None,
|
|
151
|
+
"last_release": None,
|
|
152
|
+
"source": "web",
|
|
153
|
+
"search_results": results[:4],
|
|
154
|
+
}
|
|
155
|
+
# Best-effort version extraction from the snippets — marked clearly
|
|
156
|
+
# as estimated in the card, never presented as registry fact.
|
|
157
|
+
m = re.search(r"\bversion\s+(?:is\s+)?([0-9]+\.[0-9]+(?:\.[0-9]+)?)", snippets, re.I)
|
|
158
|
+
if m:
|
|
159
|
+
info["latest_version"] = m.group(1)
|
|
160
|
+
return info
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def research_package(name, manager=None):
|
|
164
|
+
"""Researches one package across the available sources. Returns a
|
|
165
|
+
plain dict (see _pypi_info/_npm_info for fields) or None if every
|
|
166
|
+
source failed (fully offline). `manager` picks the registry API —
|
|
167
|
+
pip-family -> PyPI, npm-family -> npm registry, otherwise web."""
|
|
168
|
+
if manager in packages.MANAGERS:
|
|
169
|
+
if manager in ("pip", "pipx", "uv", "conda"):
|
|
170
|
+
return _pypi_info(name)
|
|
171
|
+
if manager in ("npm", "pnpm", "yarn", "bun"):
|
|
172
|
+
return _npm_info(name)
|
|
173
|
+
pypi = _pypi_info(name)
|
|
174
|
+
if pypi:
|
|
175
|
+
return pypi
|
|
176
|
+
npm = _npm_info(name)
|
|
177
|
+
if npm:
|
|
178
|
+
return npm
|
|
179
|
+
return _web_info(name)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ----------------------------------------------------- alternative scan --
|
|
183
|
+
# Tiny curated alternative maps for the most-researched categories, so
|
|
184
|
+
# "compare alternatives" has real data even when fully offline. Honest
|
|
185
|
+
# scope: these are static tables, not a live index.
|
|
186
|
+
_ALTERNATIVES = {
|
|
187
|
+
"http": ["requests", "httpx", "aiohttp", "urllib3"],
|
|
188
|
+
"plotting": ["matplotlib", "seaborn", "plotly", "bokeh"],
|
|
189
|
+
"dataframe": ["pandas", "polars", "dask", "pyspark"],
|
|
190
|
+
"ml": ["scikit-learn", "xgboost", "lightgbm", "catboost"],
|
|
191
|
+
"deeplearning": ["torch", "tensorflow", "jax", "paddlepaddle"],
|
|
192
|
+
"web": ["flask", "fastapi", "django", "bottle"],
|
|
193
|
+
"testing": ["pytest", "unittest", "nose", "hypothesis"],
|
|
194
|
+
"date": ["python-dateutil", "pendulum", "arrow", "dateutil"],
|
|
195
|
+
"image": ["opencv-python", "Pillow", "scikit-image", "imageio"],
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def alternatives_for(name):
|
|
200
|
+
"""[package_name, ...] in the same category as `name`, when a
|
|
201
|
+
category match exists (uses a keyword->candidates table)."""
|
|
202
|
+
low = name.lower()
|
|
203
|
+
for category, members in _ALTERNATIVES.items():
|
|
204
|
+
for member in members:
|
|
205
|
+
if member.split("/")[-1].lower() in (low,) or member == low:
|
|
206
|
+
return [m for m in members if m != low]
|
|
207
|
+
if low in category:
|
|
208
|
+
return members
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def compare_alternatives(name):
|
|
213
|
+
"""Research card rows for `name` vs its alternatives (best-effort:
|
|
214
|
+
registry data where reachable, otherwise names + 'check registry').
|
|
215
|
+
Returns a list of dicts: {name, latest, license, notes}."""
|
|
216
|
+
rows = []
|
|
217
|
+
for alt in alternatives_for(name):
|
|
218
|
+
info = research_package(alt)
|
|
219
|
+
if info:
|
|
220
|
+
rows.append({
|
|
221
|
+
"name": info.get("name", alt),
|
|
222
|
+
"latest": info.get("latest_version"),
|
|
223
|
+
"license": info.get("license"),
|
|
224
|
+
"source": info.get("source"),
|
|
225
|
+
})
|
|
226
|
+
else:
|
|
227
|
+
rows.append({"name": alt, "latest": None, "license": None, "source": None})
|
|
228
|
+
return rows
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ------------------------------------------------------------- the card --
|
|
232
|
+
def research_card(req):
|
|
233
|
+
"""Full research pass for a PackageRequest — info + comparison.
|
|
234
|
+
Returns a dict:
|
|
235
|
+
{"info": {..}, "alternatives": [..], "recommendation": str,
|
|
236
|
+
"sources": int, "unreachable": bool}
|
|
237
|
+
The `recommendation` is a short honest sentence; it never promises
|
|
238
|
+
more than the data supports."""
|
|
239
|
+
info = research_package(req.name, req.manager)
|
|
240
|
+
alts = compare_alternatives(req.name)
|
|
241
|
+
unreachable = info is None and not alts
|
|
242
|
+
recommendation = ""
|
|
243
|
+
if info:
|
|
244
|
+
latest = info.get("latest_version")
|
|
245
|
+
if latest:
|
|
246
|
+
recommendation = (f"Latest known version of '{req.name}' is "
|
|
247
|
+
f"{latest}{' (verified from ' + info.get('source', '?') + ')' if info.get('source') != 'web' else ' (estimated from web search — verify before relying on it)'}.")
|
|
248
|
+
else:
|
|
249
|
+
recommendation = (f"'{req.name}' appears in web results, but the latest "
|
|
250
|
+
"version couldn't be confirmed from a registry — check "
|
|
251
|
+
"its official page before installing.")
|
|
252
|
+
else:
|
|
253
|
+
recommendation = (f"Couldn't reach a package registry or web search for "
|
|
254
|
+
f"'{req.name}' — you may still install it, but nothing here "
|
|
255
|
+
"is verified. Proceed at your own judgment.")
|
|
256
|
+
return {
|
|
257
|
+
"info": info,
|
|
258
|
+
"alternatives": alts,
|
|
259
|
+
"recommendation": recommendation,
|
|
260
|
+
"unreachable": unreachable,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def render_card(req, card, term_width=78):
|
|
265
|
+
"""Classic-terminal renderer for a research card. Returns lines
|
|
266
|
+
(already themed)."""
|
|
267
|
+
from . import theme
|
|
268
|
+
info = card.get("info")
|
|
269
|
+
lines = []
|
|
270
|
+
if not info:
|
|
271
|
+
lines.append(theme.orange(f" No live research available for '{req.name}'."))
|
|
272
|
+
if card.get("recommendation"):
|
|
273
|
+
lines.append(theme.faint(" " + card["recommendation"]))
|
|
274
|
+
return lines
|
|
275
|
+
lines.append(theme.cyan(f" {info.get('name', req.name)}", bold=True))
|
|
276
|
+
summary = (info.get("summary") or "").strip()
|
|
277
|
+
if summary:
|
|
278
|
+
lines.append(theme.faint(" " + summary[:term_width - 4]))
|
|
279
|
+
lines.append("")
|
|
280
|
+
src = info.get("source")
|
|
281
|
+
src_tag = {"pypi": "PyPI registry", "npm": "npm registry", "web": "web search (estimated)"}[src]
|
|
282
|
+
rows = [
|
|
283
|
+
("Latest version", info.get("latest_version") or "\u2014"),
|
|
284
|
+
("License", info.get("license") or "\u2014"),
|
|
285
|
+
("Platform support", _platform_note(info)),
|
|
286
|
+
("Maintenance", _maintenance_note(info)),
|
|
287
|
+
("Compatibility", f"requires Python {info.get('requires_python')}" if info.get("requires_python") else "\u2014"),
|
|
288
|
+
]
|
|
289
|
+
for label, value in rows:
|
|
290
|
+
lines.append(f" {theme.cyan(label + ':', bold=True)} {value}")
|
|
291
|
+
lines.append("")
|
|
292
|
+
lines.append(theme.dim(f" source: {src_tag}"))
|
|
293
|
+
if info.get("home_page"):
|
|
294
|
+
lines.append(theme.dim(" docs: " + str(info["home_page"])))
|
|
295
|
+
return lines
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _platform_note(info):
|
|
299
|
+
"""Honest platform note: Python wheels/any-pure-python and npm
|
|
300
|
+
packages are cross-platform by design; the registry gives us the
|
|
301
|
+
python-version gate but not OS-level support, so we say exactly
|
|
302
|
+
that instead of inventing a matrix."""
|
|
303
|
+
src = info.get("source")
|
|
304
|
+
if src in ("pypi", "npm"):
|
|
305
|
+
return "cross-platform (registry package)"
|
|
306
|
+
return "see official docs (unverified)"
|
|
307
|
+
|
|
308
|
+
def _maintenance_note(info):
|
|
309
|
+
first = info.get("first_release")
|
|
310
|
+
last = info.get("last_release")
|
|
311
|
+
if not last:
|
|
312
|
+
return "unknown"
|
|
313
|
+
try:
|
|
314
|
+
t = time.strptime(last, "%Y-%m-%d")
|
|
315
|
+
years = (time.time() - time.mktime(t)) / (365.25 * 86400)
|
|
316
|
+
except Exception:
|
|
317
|
+
years = 99
|
|
318
|
+
if years < 1:
|
|
319
|
+
return f"active (last release {last})"
|
|
320
|
+
if years < 2:
|
|
321
|
+
return f"recent (last release {last})"
|
|
322
|
+
return f"quiet \u2014 last release {last} ({years:.0f} years ago)"
|