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
@@ -0,0 +1,386 @@
1
+ jinx_name: semantic_scholar
2
+ description: Search Semantic Scholar with interactive TUI. Requires S2_API_KEY env var.
3
+ inputs:
4
+ - query: ""
5
+ - limit: "20"
6
+ - text: "false"
7
+
8
+ steps:
9
+ - name: search_s2
10
+ engine: python
11
+ code: |
12
+ import os
13
+ import sys
14
+ import tty
15
+ import termios
16
+ import time
17
+ import requests
18
+ import webbrowser
19
+ import subprocess
20
+
21
+ query = context.get('query', '')
22
+ limit = int(context.get('limit', 20))
23
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
24
+
25
+ if not query:
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}"
@@ -1,7 +1,8 @@
1
1
  jinx_name: "sleep"
2
- description: "Evolve knowledge graph. Use --dream to also run creative synthesis."
2
+ description: "Evolve knowledge graph. Use --dream for creative synthesis, --backfill to import approved memories."
3
3
  inputs:
4
4
  - dream: False
5
+ - backfill: False
5
6
  - ops: ""
6
7
  - model: ""
7
8
  - provider: ""
@@ -12,10 +13,10 @@ steps:
12
13
  import os
13
14
  import traceback
14
15
  from npcpy.memory.command_history import CommandHistory, load_kg_from_db, save_kg_to_db
15
- from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process
16
- # Assuming render_markdown is available if needed for logging progress
16
+ from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process, kg_backfill_from_memories
17
17
 
18
18
  is_dreaming = context.get('dream')
19
+ do_backfill = context.get('backfill')
19
20
  operations_str = context.get('ops')
20
21
  llm_model = context.get('model')
21
22
  llm_provider = context.get('provider')
@@ -26,25 +27,22 @@ steps:
26
27
  operations_config = None
27
28
  if operations_str and isinstance(operations_str, str):
28
29
  operations_config = [op.strip() for op in operations_str.split(',')]
29
-
30
+
30
31
  # Fallback for model/provider if not explicitly set in Jinx inputs
31
32
  if not llm_model and current_npc and current_npc.model:
32
33
  llm_model = current_npc.model
33
34
  if not llm_provider and current_npc and current_npc.provider:
34
35
  llm_provider = current_npc.provider
35
-
36
+
36
37
  # Final fallbacks from state
37
38
  if not llm_model: llm_model = state.chat_model if state else "llama3.2"
38
39
  if not llm_provider: llm_provider = state.chat_provider if state else "ollama"
39
40
 
40
41
  team_name = current_team.name if current_team else "__none__"
41
- npc_name = current_npc.name if isinstance(current_npc, type(None).__class__) else "__none__"
42
+ npc_name = current_npc.name if current_npc else "__none__"
42
43
  current_path = os.getcwd()
43
44
  scope_str = f"Team: '{team_name}', NPC: '{npc_name}', Path: '{current_path}'"
44
45
 
45
- # Assume render_markdown exists
46
- # render_markdown(f"- Checking knowledge graph for scope: {scope_str}")
47
-
48
46
  command_history = None
49
47
  try:
50
48
  db_path = os.getenv("NPCSH_DB_PATH", os.path.expanduser("~/npcsh_history.db"))
@@ -57,13 +55,26 @@ steps:
57
55
 
58
56
  output_result = ""
59
57
  try:
58
+ # Run backfill first if requested
59
+ if do_backfill:
60
+ print("Running backfill from approved memories...")
61
+ stats = kg_backfill_from_memories(
62
+ engine,
63
+ model=llm_model,
64
+ provider=llm_provider,
65
+ npc=current_npc,
66
+ get_concepts=True,
67
+ dry_run=False
68
+ )
69
+ output_result += f"Backfill: +{stats['facts_after'] - stats['facts_before']} facts, +{stats['concepts_after'] - stats['concepts_before']} concepts\n"
70
+
60
71
  current_kg = load_kg_from_db(engine, team_name, npc_name, current_path)
61
72
 
62
73
  if not current_kg or not current_kg.get('facts'):
63
74
  output_msg = f"Knowledge graph for the current scope is empty. Nothing to process.\n"
64
75
  output_msg += f" - Scope Checked: {scope_str}\n\n"
65
- output_msg += "**Hint:** Have a conversation or run some commands first to build up knowledge in this specific context. The KG is unique to each combination of Team, NPC, and directory."
66
- context['output'] = output_msg
76
+ output_msg += "**Hint:** Run `/sleep backfill=true` to import approved memories, or have conversations first."
77
+ context['output'] = output_result + output_msg if output_result else output_msg
67
78
  context['messages'] = output_messages
68
79
  exit()
69
80