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.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. 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)"