npcsh 1.1.17__py3-none-any.whl → 1.1.18__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 (169) hide show
  1. npcsh/_state.py +114 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +46 -12
  5. npcsh/benchmark/runner.py +85 -43
  6. npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
  7. npcsh/build.py +2 -4
  8. npcsh/completion.py +2 -6
  9. npcsh/config.py +1 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/execution.py +0 -1
  13. npcsh/guac.py +0 -1
  14. npcsh/mcp_helpers.py +2 -3
  15. npcsh/mcp_server.py +5 -10
  16. npcsh/npc.py +10 -11
  17. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  18. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  19. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  20. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  21. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  22. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  23. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  24. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  25. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  26. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  27. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  28. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  29. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  30. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  31. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  32. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  33. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  34. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  35. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  36. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  37. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  38. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  39. npcsh/npcsh.py +7 -4
  40. npcsh/plonk.py +0 -1
  41. npcsh/pti.py +0 -1
  42. npcsh/routes.py +1 -3
  43. npcsh/spool.py +0 -1
  44. npcsh/ui.py +0 -1
  45. npcsh/wander.py +0 -1
  46. npcsh/yap.py +0 -1
  47. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  48. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  49. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  50. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  51. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  52. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  53. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  54. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  55. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  56. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  57. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  58. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  59. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  60. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  61. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  62. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  63. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  64. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  65. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  66. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  67. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  68. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.jinx +13 -7
  69. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/METADATA +90 -1
  70. npcsh-1.1.18.dist-info/RECORD +235 -0
  71. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  72. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +0 -3
  73. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  74. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  75. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  76. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  77. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  78. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  79. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  80. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  81. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  82. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  83. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  84. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  85. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  86. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  87. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  88. npcsh-1.1.17.dist-info/RECORD +0 -219
  89. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  90. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  91. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  92. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  93. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  94. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +0 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +0 -0
  96. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +0 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  98. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  100. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +0 -0
  102. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +0 -0
  103. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  104. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +0 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  106. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  107. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  108. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  109. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  110. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  112. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  113. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  114. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  115. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +0 -0
  116. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  117. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +0 -0
  118. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  119. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  120. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  121. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  122. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  123. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  124. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  125. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/notify.jinx +0 -0
  126. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  127. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  128. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +0 -0
  129. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  130. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  131. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +0 -0
  132. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  133. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  134. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  135. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/search.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  168. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  169. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -1,69 +1,386 @@
1
1
  jinx_name: semantic_scholar
2
- description: Search Semantic Scholar for academic papers. Requires S2_API_KEY env var.
2
+ description: Search Semantic Scholar with interactive TUI. Requires S2_API_KEY env var.
3
3
  inputs:
4
4
  - query: ""
5
- - limit: 10
5
+ - limit: "20"
6
+ - text: "false"
7
+
6
8
  steps:
7
9
  - name: search_s2
8
10
  engine: python
9
11
  code: |
10
12
  import os
13
+ import sys
14
+ import tty
15
+ import termios
11
16
  import time
12
17
  import requests
18
+ import webbrowser
19
+ import subprocess
13
20
 
14
21
  query = context.get('query', '')
15
- limit = int(context.get('limit', 10))
22
+ limit = int(context.get('limit', 20))
23
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
16
24
 
17
25
  if not query:
18
- context['output'] = "Usage: /semantic_scholar <query> [--limit N]"
19
- exit()
20
-
21
- api_key = os.environ.get('S2_API_KEY')
22
- if not api_key:
23
- context['output'] = "Error: S2_API_KEY environment variable not set. Get one at https://www.semanticscholar.org/product/api"
24
- exit()
25
-
26
- url = "https://api.semanticscholar.org/graph/v1/paper/search"
27
- headers = {"x-api-key": api_key}
28
- params = {
29
- "query": query,
30
- "limit": limit,
31
- "fields": "title,abstract,authors,year,citationCount,url,tldr"
32
- }
33
-
34
- try:
35
- response = requests.get(url, headers=headers, params=params, timeout=30)
36
- response.raise_for_status()
37
- data = response.json().get('data', [])
38
-
39
- if not data:
40
- context['output'] = f"No papers found for: {query}"
41
- exit()
42
-
43
- results = []
44
- for i, paper in enumerate(data, 1):
45
- title = paper.get('title', 'No title')
46
- year = paper.get('year', '?')
47
- citations = paper.get('citationCount', 0)
48
- authors = ', '.join([a.get('name', '') for a in paper.get('authors', [])[:3]])
49
- if len(paper.get('authors', [])) > 3:
50
- authors += ' et al.'
51
- abstract = paper.get('abstract', '')[:200] + '...' if paper.get('abstract') else 'No abstract'
52
- tldr = paper.get('tldr', {}).get('text', '') if paper.get('tldr') else ''
53
- url = paper.get('url', '')
54
-
55
- results.append(f"{i}. {title} ({year})")
56
- results.append(f" Authors: {authors}")
57
- results.append(f" Citations: {citations}")
58
- if tldr:
59
- results.append(f" TL;DR: {tldr}")
60
- else:
61
- results.append(f" Abstract: {abstract}")
62
- results.append(f" URL: {url}")
63
- results.append("")
64
-
65
- context['output'] = f"Found {len(data)} papers:\n\n" + "\n".join(results)
66
- context['papers'] = data
67
-
68
- except requests.exceptions.RequestException as e:
69
- context['output'] = f"Semantic Scholar API error: {e}"
26
+ lines = [
27
+ "Usage: /semantic_scholar <query>",
28
+ "",
29
+ "Options:",
30
+ " limit - Max results (default 20)",
31
+ " text - Text-only output, no TUI (true/false)",
32
+ "",
33
+ "TUI Controls:",
34
+ " j/k or arrows - Navigate",
35
+ " 1/2/3 - Sort by year/citations/author",
36
+ " y - Filter by year",
37
+ " p - Preview abstract/TL;DR",
38
+ " o - Open in browser",
39
+ " i - Open in incognide",
40
+ " d - Download paper info",
41
+ " q/ESC - Quit",
42
+ "",
43
+ "Examples:",
44
+ " /semantic_scholar machine learning",
45
+ " /semantic_scholar neural networks limit=50",
46
+ ]
47
+ context['output'] = "\n".join(lines)
48
+ else:
49
+ api_key = os.environ.get('S2_API_KEY')
50
+ if not api_key:
51
+ context['output'] = "Error: S2_API_KEY environment variable not set.\nGet one at https://www.semanticscholar.org/product/api"
52
+ else:
53
+ url = "https://api.semanticscholar.org/graph/v1/paper/search"
54
+ headers = {"x-api-key": api_key}
55
+ params = {
56
+ "query": query,
57
+ "limit": limit,
58
+ "fields": "title,abstract,authors,year,citationCount,url,tldr,venue,openAccessPdf"
59
+ }
60
+
61
+ try:
62
+ response = requests.get(url, headers=headers, params=params, timeout=30)
63
+ response.raise_for_status()
64
+ papers = response.json().get('data', [])
65
+
66
+ if not papers:
67
+ context['output'] = f"No papers found for: {query}"
68
+ elif text_mode:
69
+ # Text-only output
70
+ results = []
71
+ for i, paper in enumerate(papers, 1):
72
+ title = paper.get('title', 'No title')
73
+ year = paper.get('year', '?')
74
+ citations = paper.get('citationCount', 0)
75
+ authors = ', '.join([a.get('name', '') for a in paper.get('authors', [])[:3]])
76
+ if len(paper.get('authors', [])) > 3:
77
+ authors += ' et al.'
78
+ abstract = paper.get('abstract', '')[:200] + '...' if paper.get('abstract') else 'No abstract'
79
+ tldr = paper.get('tldr', {}).get('text', '') if paper.get('tldr') else ''
80
+ paper_url = paper.get('url', '')
81
+
82
+ results.append(f"{i}. {title} ({year})")
83
+ results.append(f" Authors: {authors}")
84
+ results.append(f" Citations: {citations}")
85
+ if tldr:
86
+ results.append(f" TL;DR: {tldr}")
87
+ else:
88
+ results.append(f" Abstract: {abstract}")
89
+ results.append(f" URL: {paper_url}")
90
+ results.append("")
91
+
92
+ context['output'] = f"Found {len(papers)} papers:\n\n" + "\n".join(results)
93
+ context['papers'] = papers
94
+ else:
95
+ # Interactive TUI mode
96
+ def get_terminal_size():
97
+ try:
98
+ size = os.get_terminal_size()
99
+ return size.columns, size.lines
100
+ except:
101
+ return 80, 24
102
+
103
+ width, height = get_terminal_size()
104
+ selected = 0
105
+ scroll = 0
106
+ list_height = height - 5
107
+ mode = 'list'
108
+ preview_scroll = 0
109
+ sort_mode = 'relevance' # relevance, year, citations, author
110
+ year_filter = None
111
+
112
+ def sort_papers(papers, sort_mode):
113
+ if sort_mode == 'year':
114
+ return sorted(papers, key=lambda x: x.get('year') or 0, reverse=True)
115
+ elif sort_mode == 'citations':
116
+ return sorted(papers, key=lambda x: x.get('citationCount') or 0, reverse=True)
117
+ elif sort_mode == 'author':
118
+ return sorted(papers, key=lambda x: (x.get('authors', [{}])[0].get('name', '') if x.get('authors') else ''))
119
+ return papers # relevance = original order
120
+
121
+ def filter_papers(papers, year_filter):
122
+ if not year_filter:
123
+ return papers
124
+ return [p for p in papers if p.get('year') and p.get('year') >= year_filter]
125
+
126
+ display_papers = filter_papers(sort_papers(papers, sort_mode), year_filter)
127
+
128
+ fd = sys.stdin.fileno()
129
+ old_settings = termios.tcgetattr(fd)
130
+
131
+ try:
132
+ tty.setcbreak(fd)
133
+ sys.stdout.write('\033[?25l')
134
+ sys.stdout.write('\033[2J\033[H')
135
+
136
+ while True:
137
+ width, height = get_terminal_size()
138
+ list_height = height - 5
139
+
140
+ if mode == 'list':
141
+ if selected < scroll:
142
+ scroll = selected
143
+ elif selected >= scroll + list_height:
144
+ scroll = selected - list_height + 1
145
+
146
+ sys.stdout.write('\033[H')
147
+
148
+ # Header
149
+ if mode == 'list':
150
+ sort_ind = {'relevance': '0', 'year': '1', 'citations': '2', 'author': '3'}[sort_mode]
151
+ yr = f" year>={year_filter}" if year_filter else ""
152
+ header = f" SEMANTIC SCHOLAR ({len(display_papers)} papers): '{query}' [sort:{sort_mode}({sort_ind}){yr}] "
153
+ else:
154
+ header = f" PREVIEW: {display_papers[selected].get('title', '')[:width-12]} "
155
+ sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
156
+
157
+ # Column headers
158
+ if mode == 'list':
159
+ col_header = f' {"YEAR":<6} {"CITE":<6} {"AUTHORS":<25} {"TITLE":<50}'
160
+ sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
161
+ else:
162
+ sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
163
+
164
+ if mode == 'list':
165
+ for i in range(list_height):
166
+ idx = scroll + i
167
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
168
+ if idx >= len(display_papers):
169
+ continue
170
+
171
+ p = display_papers[idx]
172
+ year = str(p.get('year') or '?')[:6]
173
+ citations = str(p.get('citationCount') or 0)[:6]
174
+ authors = ', '.join([a.get('name', '')[:15] for a in p.get('authors', [])[:2]])
175
+ if len(p.get('authors', [])) > 2:
176
+ authors += ' et al.'
177
+ authors = authors[:25]
178
+ title = (p.get('title') or '')[:60].replace('\n', ' ')
179
+
180
+ line = f" {year:<6} {citations:<6} {authors:<25} {title}"
181
+ line = line[:width-1]
182
+
183
+ if idx == selected:
184
+ sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
185
+ else:
186
+ sys.stdout.write(f' {line}')
187
+
188
+ # Status bar
189
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
190
+ sel = display_papers[selected] if display_papers else {}
191
+ venue = (sel.get('venue') or '')[:30]
192
+ has_pdf = 'PDF' if sel.get('openAccessPdf') else ''
193
+ sys.stdout.write(f'\033[{height-1};1H\033[K {venue} {has_pdf}'.ljust(width))
194
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav 0/1/2/3:Sort y:Year p:Preview o:Open i:Incog d:Download q:Quit [{selected+1}/{len(display_papers)}] \033[0m')
195
+
196
+ else: # preview mode
197
+ sel = display_papers[selected]
198
+ tldr = sel.get('tldr', {}).get('text', '') if sel.get('tldr') else ''
199
+ abstract = sel.get('abstract') or 'No abstract available'
200
+
201
+ # Build preview lines
202
+ preview_lines = [
203
+ f"Title: {sel.get('title', '')}",
204
+ "",
205
+ f"Year: {sel.get('year', '?')}",
206
+ f"Citations: {sel.get('citationCount', 0)}",
207
+ f"Venue: {sel.get('venue', '')}",
208
+ "",
209
+ "Authors:",
210
+ ]
211
+ for a in sel.get('authors', []):
212
+ preview_lines.append(f" - {a.get('name', '')}")
213
+ preview_lines.append("")
214
+
215
+ if tldr:
216
+ preview_lines.append("TL;DR:")
217
+ # Word wrap TL;DR
218
+ words = tldr.split()
219
+ line = ""
220
+ for w in words:
221
+ if len(line) + len(w) + 1 > width - 4:
222
+ preview_lines.append(f" {line}")
223
+ line = w
224
+ else:
225
+ line = f"{line} {w}" if line else w
226
+ if line:
227
+ preview_lines.append(f" {line}")
228
+ preview_lines.append("")
229
+
230
+ preview_lines.append("Abstract:")
231
+ # Word wrap abstract
232
+ words = abstract.split()
233
+ line = ""
234
+ for w in words:
235
+ if len(line) + len(w) + 1 > width - 4:
236
+ preview_lines.append(f" {line}")
237
+ line = w
238
+ else:
239
+ line = f"{line} {w}" if line else w
240
+ if line:
241
+ preview_lines.append(f" {line}")
242
+
243
+ preview_lines.append("")
244
+ preview_lines.append(f"URL: {sel.get('url', '')}")
245
+ if sel.get('openAccessPdf'):
246
+ preview_lines.append(f"PDF: {sel.get('openAccessPdf', {}).get('url', '')}")
247
+
248
+ for i in range(list_height):
249
+ idx = preview_scroll + i
250
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
251
+ if idx < len(preview_lines):
252
+ sys.stdout.write(preview_lines[idx][:width-1])
253
+
254
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
255
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(preview_lines)} lines]')
256
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back o:Open d:Download q:Quit \033[0m')
257
+
258
+ sys.stdout.flush()
259
+
260
+ c = sys.stdin.read(1)
261
+
262
+ if c == '\x1b':
263
+ c2 = sys.stdin.read(1)
264
+ if c2 == '[':
265
+ c3 = sys.stdin.read(1)
266
+ if c3 == 'A': # Up
267
+ if mode == 'list' and selected > 0:
268
+ selected -= 1
269
+ elif mode == 'preview' and preview_scroll > 0:
270
+ preview_scroll -= 1
271
+ elif c3 == 'B': # Down
272
+ if mode == 'list' and selected < len(display_papers) - 1:
273
+ selected += 1
274
+ elif mode == 'preview' and preview_scroll < 100:
275
+ preview_scroll += 1
276
+ else:
277
+ if mode == 'preview':
278
+ mode = 'list'
279
+ sys.stdout.write('\033[2J\033[H')
280
+ else:
281
+ context['output'] = "Cancelled."
282
+ break
283
+ continue
284
+
285
+ if c == 'q' or c == '\x03':
286
+ context['output'] = "Cancelled."
287
+ break
288
+ elif c == 'k':
289
+ if mode == 'list' and selected > 0:
290
+ selected -= 1
291
+ elif mode == 'preview' and preview_scroll > 0:
292
+ preview_scroll -= 1
293
+ elif c == 'j':
294
+ if mode == 'list' and selected < len(display_papers) - 1:
295
+ selected += 1
296
+ elif mode == 'preview' and preview_scroll < 100:
297
+ preview_scroll += 1
298
+ elif c == '0':
299
+ sort_mode = 'relevance'
300
+ display_papers = filter_papers(sort_papers(papers, sort_mode), year_filter)
301
+ selected = 0
302
+ scroll = 0
303
+ elif c == '1':
304
+ sort_mode = 'year'
305
+ display_papers = filter_papers(sort_papers(papers, sort_mode), year_filter)
306
+ selected = 0
307
+ scroll = 0
308
+ elif c == '2':
309
+ sort_mode = 'citations'
310
+ display_papers = filter_papers(sort_papers(papers, sort_mode), year_filter)
311
+ selected = 0
312
+ scroll = 0
313
+ elif c == '3':
314
+ sort_mode = 'author'
315
+ display_papers = filter_papers(sort_papers(papers, sort_mode), year_filter)
316
+ selected = 0
317
+ scroll = 0
318
+ elif c == 'y' and mode == 'list':
319
+ # Cycle through year filters
320
+ import datetime
321
+ current_year = datetime.datetime.now().year
322
+ if year_filter is None:
323
+ year_filter = current_year - 1
324
+ elif year_filter == current_year - 1:
325
+ year_filter = current_year - 3
326
+ elif year_filter == current_year - 3:
327
+ year_filter = current_year - 5
328
+ else:
329
+ year_filter = None
330
+ display_papers = filter_papers(sort_papers(papers, sort_mode), year_filter)
331
+ selected = 0
332
+ scroll = 0
333
+ elif c == 'p' and mode == 'list' and display_papers:
334
+ mode = 'preview'
335
+ preview_scroll = 0
336
+ sys.stdout.write('\033[2J\033[H')
337
+ elif c == 'b' and mode == 'preview':
338
+ mode = 'list'
339
+ sys.stdout.write('\033[2J\033[H')
340
+ elif c == 'o' and display_papers:
341
+ sel = display_papers[selected]
342
+ paper_url = sel.get('url', '')
343
+ if paper_url:
344
+ webbrowser.open(paper_url)
345
+ elif c == 'i' and display_papers:
346
+ sel = display_papers[selected]
347
+ paper_url = sel.get('url', '')
348
+ if paper_url:
349
+ try:
350
+ subprocess.run(['npcsh', '-c', f'/navigate url={paper_url}'], check=False, capture_output=True)
351
+ except:
352
+ webbrowser.open(paper_url)
353
+ elif c == 'd' and display_papers:
354
+ sel = display_papers[selected]
355
+ # Save paper info to file
356
+ title = sel.get('title', 'paper')
357
+ safe_title = "".join(c if c.isalnum() or c in ' -_' else '_' for c in title)[:50]
358
+ filename = f"{safe_title}.txt"
359
+ with open(filename, 'w') as f:
360
+ f.write(f"Title: {sel.get('title', '')}\n")
361
+ f.write(f"Year: {sel.get('year', '')}\n")
362
+ f.write(f"Citations: {sel.get('citationCount', 0)}\n")
363
+ f.write(f"Venue: {sel.get('venue', '')}\n")
364
+ f.write(f"Authors: {', '.join([a.get('name', '') for a in sel.get('authors', [])])}\n")
365
+ f.write(f"URL: {sel.get('url', '')}\n")
366
+ if sel.get('openAccessPdf'):
367
+ f.write(f"PDF: {sel.get('openAccessPdf', {}).get('url', '')}\n")
368
+ f.write(f"\nAbstract:\n{sel.get('abstract', '')}\n")
369
+ if sel.get('tldr'):
370
+ f.write(f"\nTL;DR:\n{sel.get('tldr', {}).get('text', '')}\n")
371
+ context['output'] = f"Saved to: {filename}"
372
+ break
373
+ elif c in ('\r', '\n') and display_papers:
374
+ sel = display_papers[selected]
375
+ context['output'] = f"Selected: {sel.get('title', '')}\nURL: {sel.get('url', '')}"
376
+ context['selected_paper'] = sel
377
+ break
378
+
379
+ finally:
380
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
381
+ sys.stdout.write('\033[?25h')
382
+ sys.stdout.write('\033[2J\033[H')
383
+ sys.stdout.flush()
384
+
385
+ except requests.exceptions.RequestException as e:
386
+ context['output'] = f"Semantic Scholar API error: {e}"