npcsh 1.1.17__py3-none-any.whl → 1.1.19__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 (197) hide show
  1. npcsh/_state.py +122 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +87 -22
  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 +2 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/diff_viewer.py +452 -0
  13. npcsh/execution.py +0 -1
  14. npcsh/guac.py +0 -1
  15. npcsh/mcp_helpers.py +2 -3
  16. npcsh/mcp_server.py +5 -10
  17. npcsh/npc.py +10 -11
  18. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  19. npcsh/npc_team/jinxs/bin/config_tui.jinx +299 -0
  20. npcsh/npc_team/jinxs/bin/memories.jinx +316 -0
  21. npcsh/npc_team/jinxs/bin/setup.jinx +240 -0
  22. npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
  23. npcsh/npc_team/jinxs/bin/team_tui.jinx +327 -0
  24. npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
  25. npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
  26. npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
  27. npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
  28. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
  29. npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
  30. npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
  31. npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
  32. npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
  33. npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
  34. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
  35. npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
  36. npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
  37. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
  38. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
  39. npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
  40. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
  41. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  42. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  43. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  44. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  45. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  46. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  47. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  48. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  49. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  50. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  51. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  52. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  53. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  54. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  55. npcsh/npc_team/jinxs/modes/guac.jinx +542 -0
  56. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  57. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  58. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  59. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  60. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  61. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  62. npcsh/npcsh.py +7 -4
  63. npcsh/plonk.py +0 -1
  64. npcsh/pti.py +0 -1
  65. npcsh/routes.py +1 -3
  66. npcsh/spool.py +0 -1
  67. npcsh/ui.py +0 -1
  68. npcsh/wander.py +0 -1
  69. npcsh/yap.py +0 -1
  70. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
  71. npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  72. npcsh-1.1.19.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  73. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  74. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
  75. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
  76. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +299 -0
  77. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
  78. npcsh-1.1.19.data/data/npcsh/npc_team/corca.jinx +430 -0
  79. npcsh-1.1.19.data/data/npcsh/npc_team/db_search.jinx +348 -0
  80. npcsh-1.1.19.data/data/npcsh/npc_team/file_search.jinx +339 -0
  81. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
  82. npcsh-1.1.19.data/data/npcsh/npc_team/guac.jinx +542 -0
  83. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  84. npcsh-1.1.19.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  85. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
  86. npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  87. npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  88. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
  89. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
  90. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
  91. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
  92. npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  93. npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +379 -0
  94. npcsh-1.1.19.data/data/npcsh/npc_team/pti.jinx +357 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
  96. npcsh-1.1.19.data/data/npcsh/npc_team/reattach.jinx +291 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
  98. npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/send_message.jinx +1 -1
  100. npcsh-1.1.19.data/data/npcsh/npc_team/setup.jinx +240 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  102. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
  103. npcsh-1.1.19.data/data/npcsh/npc_team/spool.jinx +350 -0
  104. npcsh-1.1.19.data/data/npcsh/npc_team/sql.jinx +20 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
  106. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
  107. npcsh-1.1.19.data/data/npcsh/npc_team/sync.jinx +223 -0
  108. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +327 -0
  109. npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +455 -0
  110. npcsh-1.1.19.data/data/npcsh/npc_team/web_search.jinx +283 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
  112. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +13 -7
  113. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
  114. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/METADATA +110 -14
  115. npcsh-1.1.19.dist-info/RECORD +244 -0
  116. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/WHEEL +1 -1
  117. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -3
  118. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  119. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  120. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  121. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  122. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  123. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  124. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  125. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  126. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  127. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  128. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  129. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  130. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  131. npcsh-1.1.17.data/data/npcsh/npc_team/sync.jinx +0 -230
  132. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  133. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  134. npcsh-1.1.17.dist-info/RECORD +0 -219
  135. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  168. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
  169. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
  170. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
  171. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
  172. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  173. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  174. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
  175. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
  176. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
  177. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  178. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
  179. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
  180. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
  181. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
  182. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
  183. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  184. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
  185. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
  186. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
  187. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
  188. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  189. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  190. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  191. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
  192. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  193. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  194. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
  195. {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
  196. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
  197. {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -1,4 +1,4 @@
1
- jinx_name: studio.split_pane
1
+ jinx_name: studio_split_pane
2
2
  description: Split an existing pane to create a new pane alongside it.
3
3
  inputs:
4
4
  - paneId: "active"
@@ -0,0 +1,350 @@
1
+ jinx_name: spool
2
+ description: Interactive chat mode - simple conversational interface with an NPC
3
+ inputs:
4
+ - model: null
5
+ - provider: null
6
+ - attachments: null
7
+ - stream: true
8
+
9
+ steps:
10
+ - name: spool_repl
11
+ engine: python
12
+ code: |
13
+ import os
14
+ import sys
15
+ import tty
16
+ import termios
17
+ from termcolor import colored
18
+
19
+ from npcpy.llm_funcs import get_llm_response
20
+ from npcpy.npc_sysenv import get_system_message, render_markdown
21
+ from npcpy.data.load import load_file_contents
22
+ from npcpy.data.text import rag_search
23
+
24
+ npc = context.get('npc')
25
+ team = context.get('team')
26
+ messages = context.get('messages', [])
27
+ stream = context.get('stream', True)
28
+ attachments = context.get('attachments')
29
+
30
+ # Resolve npc if it's a string (npc name) rather than NPC object
31
+ if isinstance(npc, str) and team:
32
+ npc = team.get(npc) if hasattr(team, 'get') else None
33
+ elif isinstance(npc, str):
34
+ npc = None
35
+
36
+ # Use NPC's model/provider or fallback
37
+ model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
38
+ provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
39
+
40
+ # ========== TUI Helper Functions ==========
41
+ def get_terminal_size():
42
+ try:
43
+ size = os.get_terminal_size()
44
+ return size.columns, size.lines
45
+ except:
46
+ return 80, 24
47
+
48
+ def history_tui_browser(messages):
49
+ """Interactive TUI browser for conversation history"""
50
+ # Filter out system messages for display
51
+ history = [m for m in messages if m.get('role') != 'system']
52
+
53
+ if not history:
54
+ print(colored("No conversation history yet.", "yellow"))
55
+ return
56
+
57
+ width, height = get_terminal_size()
58
+ selected = len(history) - 1 # Start at most recent
59
+ scroll = max(0, selected - (height - 6))
60
+ list_height = height - 5
61
+ mode = 'list'
62
+ preview_scroll = 0
63
+ preview_lines = []
64
+
65
+ fd = sys.stdin.fileno()
66
+ old_settings = termios.tcgetattr(fd)
67
+
68
+ try:
69
+ tty.setcbreak(fd)
70
+ sys.stdout.write('\033[?25l')
71
+ sys.stdout.write('\033[2J\033[H')
72
+
73
+ while True:
74
+ width, height = get_terminal_size()
75
+ list_height = height - 5
76
+
77
+ if mode == 'list':
78
+ if selected < scroll:
79
+ scroll = selected
80
+ elif selected >= scroll + list_height:
81
+ scroll = selected - list_height + 1
82
+
83
+ sys.stdout.write('\033[H')
84
+
85
+ # Header
86
+ if mode == 'list':
87
+ header = f" CONVERSATION HISTORY ({len(history)} messages) "
88
+ else:
89
+ header = f" MESSAGE {selected + 1} "
90
+ sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
91
+
92
+ if mode == 'list':
93
+ col_header = f' {"#":<4} {"ROLE":<12} {"PREVIEW":<60}'
94
+ sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
95
+ else:
96
+ sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
97
+
98
+ if mode == 'list':
99
+ for i in range(list_height):
100
+ idx = scroll + i
101
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
102
+ if idx >= len(history):
103
+ continue
104
+
105
+ msg = history[idx]
106
+ role = msg.get('role', '?')[:12]
107
+ content = msg.get('content', '')[:60].replace('\n', ' ')
108
+
109
+ line = f" {idx+1:<4} {role:<12} {content}"
110
+ line = line[:width-1]
111
+
112
+ # Color by role
113
+ if msg.get('role') == 'user':
114
+ color = '\033[32m' # green
115
+ elif msg.get('role') == 'assistant':
116
+ color = '\033[34m' # blue
117
+ else:
118
+ color = ''
119
+
120
+ if idx == selected:
121
+ sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
122
+ elif color:
123
+ sys.stdout.write(f'{color}{line}\033[0m')
124
+ else:
125
+ sys.stdout.write(f' {line}')
126
+
127
+ # Status bar
128
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
129
+ user_msgs = len([m for m in history if m.get('role') == 'user'])
130
+ asst_msgs = len([m for m in history if m.get('role') == 'assistant'])
131
+ sys.stdout.write(f'\033[{height-1};1H\033[K User: {user_msgs} | Assistant: {asst_msgs}'.ljust(width)[:width])
132
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav p:Preview c:Copy q:Quit [{selected+1}/{len(history)}] \033[0m')
133
+
134
+ else: # preview mode
135
+ for i in range(list_height):
136
+ idx = preview_scroll + i
137
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
138
+ if idx < len(preview_lines):
139
+ sys.stdout.write(preview_lines[idx][:width-1])
140
+
141
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
142
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(preview_lines)} lines]')
143
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back c:Copy q:Quit \033[0m')
144
+
145
+ sys.stdout.flush()
146
+
147
+ c = sys.stdin.read(1)
148
+
149
+ if c == '\x1b':
150
+ c2 = sys.stdin.read(1)
151
+ if c2 == '[':
152
+ c3 = sys.stdin.read(1)
153
+ if c3 == 'A': # Up
154
+ if mode == 'list' and selected > 0:
155
+ selected -= 1
156
+ elif mode == 'preview' and preview_scroll > 0:
157
+ preview_scroll -= 1
158
+ elif c3 == 'B': # Down
159
+ if mode == 'list' and selected < len(history) - 1:
160
+ selected += 1
161
+ elif mode == 'preview' and preview_scroll < max(0, len(preview_lines) - list_height):
162
+ preview_scroll += 1
163
+ else:
164
+ if mode == 'preview':
165
+ mode = 'list'
166
+ sys.stdout.write('\033[2J\033[H')
167
+ else:
168
+ return
169
+ continue
170
+
171
+ if c == 'q' or c == '\x03':
172
+ return
173
+ elif c == 'k':
174
+ if mode == 'list' and selected > 0:
175
+ selected -= 1
176
+ elif mode == 'preview' and preview_scroll > 0:
177
+ preview_scroll -= 1
178
+ elif c == 'j':
179
+ if mode == 'list' and selected < len(history) - 1:
180
+ selected += 1
181
+ elif mode == 'preview' and preview_scroll < max(0, len(preview_lines) - list_height):
182
+ preview_scroll += 1
183
+ elif c == 'p' and mode == 'list':
184
+ # Preview message
185
+ msg = history[selected]
186
+ preview_str = f"Role: {msg.get('role', '?')}\n"
187
+ preview_str += f"{'=' * 40}\n\n"
188
+ preview_str += msg.get('content', '')
189
+ preview_lines = preview_str.split('\n')
190
+ mode = 'preview'
191
+ preview_scroll = 0
192
+ sys.stdout.write('\033[2J\033[H')
193
+ elif c == 'b' and mode == 'preview':
194
+ mode = 'list'
195
+ sys.stdout.write('\033[2J\033[H')
196
+ elif c == 'c':
197
+ # Copy to clipboard
198
+ msg = history[selected]
199
+ content = msg.get('content', '')
200
+ try:
201
+ import subprocess
202
+ subprocess.run(['xclip', '-selection', 'clipboard'], input=content.encode(), check=True)
203
+ except:
204
+ try:
205
+ subprocess.run(['xsel', '--clipboard', '--input'], input=content.encode(), check=True)
206
+ except:
207
+ pass
208
+
209
+ finally:
210
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
211
+ sys.stdout.write('\033[?25h')
212
+ sys.stdout.write('\033[2J\033[H')
213
+ sys.stdout.flush()
214
+
215
+ # ASCII art
216
+ print("""
217
+ _____ ____ ____ ____ _
218
+ / ___/| _ \ / __ \ / __ \| |
219
+ \___ \| |_) | | | | | | | |
220
+ ___) | __/| | | | | | | |___
221
+ |____/|_| \____/ \____/|_____|
222
+ """)
223
+
224
+ npc_name = npc.name if npc else "chat"
225
+ print(f"Entering spool mode (NPC: {npc_name}). Type '/sq' to exit.")
226
+ print(" - /history: Browse conversation history")
227
+
228
+ # Load attachments if provided
229
+ loaded_chunks = {}
230
+ if attachments:
231
+ if isinstance(attachments, str):
232
+ attachments = [f.strip() for f in attachments.split(',')]
233
+ for file_path in attachments:
234
+ file_path = os.path.expanduser(file_path)
235
+ if os.path.exists(file_path):
236
+ try:
237
+ chunks = load_file_contents(file_path)
238
+ loaded_chunks[file_path] = chunks
239
+ print(colored(f"Loaded {len(chunks)} chunks from: {file_path}", "green"))
240
+ except Exception as e:
241
+ print(colored(f"Error loading {file_path}: {e}", "red"))
242
+
243
+ # Ensure system message
244
+ if not messages or messages[0].get("role") != "system":
245
+ sys_msg = get_system_message(npc) if npc else "You are a helpful assistant."
246
+ messages.insert(0, {"role": "system", "content": sys_msg})
247
+
248
+ # REPL loop
249
+ while True:
250
+ try:
251
+ prompt_str = f"{npc_name}> "
252
+ user_input = input(prompt_str).strip()
253
+
254
+ if not user_input:
255
+ continue
256
+
257
+ if user_input.lower() == "/sq":
258
+ print("Exiting spool mode.")
259
+ break
260
+
261
+ # Handle /history for conversation browser
262
+ if user_input.lower() == "/history":
263
+ history_tui_browser(messages)
264
+ continue
265
+
266
+ # Handle /ots for screenshots inline
267
+ if user_input.startswith("/ots"):
268
+ from npcpy.data.image import capture_screenshot
269
+ parts = user_input.split()
270
+ image_paths = []
271
+ if len(parts) > 1:
272
+ for p in parts[1:]:
273
+ fp = os.path.expanduser(p)
274
+ if os.path.exists(fp):
275
+ image_paths.append(fp)
276
+ else:
277
+ ss = capture_screenshot()
278
+ if ss and "file_path" in ss:
279
+ image_paths.append(ss["file_path"])
280
+ print(colored(f"Screenshot: {ss['filename']}", "green"))
281
+
282
+ if image_paths:
283
+ vision_prompt = input("Prompt for image(s): ").strip() or "Describe these images."
284
+ resp = get_llm_response(
285
+ vision_prompt,
286
+ model=npc.vision_model if hasattr(npc, 'vision_model') else model,
287
+ provider=npc.vision_provider if hasattr(npc, 'vision_provider') else provider,
288
+ messages=messages,
289
+ images=image_paths,
290
+ stream=stream,
291
+ npc=npc
292
+ )
293
+ messages = resp.get('messages', messages)
294
+ render_markdown(str(resp.get('response', '')))
295
+ continue
296
+
297
+ # Add RAG context if files loaded
298
+ current_prompt = user_input
299
+ if loaded_chunks:
300
+ context_content = ""
301
+ for filename, chunks in loaded_chunks.items():
302
+ full_text = "\n".join(chunks)
303
+ retrieved = rag_search(user_input, full_text, similarity_threshold=0.3)
304
+ if retrieved:
305
+ context_content += f"\n\nContext from {filename}:\n{retrieved}\n"
306
+ if context_content:
307
+ current_prompt += f"\n\n--- Relevant context ---{context_content}"
308
+
309
+ # Get response
310
+ resp = get_llm_response(
311
+ current_prompt,
312
+ model=model,
313
+ provider=provider,
314
+ messages=messages,
315
+ stream=stream,
316
+ npc=npc
317
+ )
318
+
319
+ messages = resp.get('messages', messages)
320
+ response_text = resp.get('response', '')
321
+
322
+ # Handle streaming vs non-streaming
323
+ if hasattr(response_text, '__iter__') and not isinstance(response_text, str):
324
+ full_response = ""
325
+ for chunk in response_text:
326
+ if hasattr(chunk, 'choices') and chunk.choices:
327
+ delta = chunk.choices[0].delta
328
+ if hasattr(delta, 'content') and delta.content:
329
+ print(delta.content, end='', flush=True)
330
+ full_response += delta.content
331
+ print()
332
+ else:
333
+ render_markdown(str(response_text))
334
+
335
+ # Track usage if available
336
+ if 'usage' in resp and npc and hasattr(npc, 'shared_context'):
337
+ usage = resp['usage']
338
+ npc.shared_context['session_input_tokens'] += usage.get('input_tokens', 0)
339
+ npc.shared_context['session_output_tokens'] += usage.get('output_tokens', 0)
340
+ npc.shared_context['turn_count'] += 1
341
+
342
+ except KeyboardInterrupt:
343
+ print("\nUse '/sq' to exit or continue.")
344
+ continue
345
+ except EOFError:
346
+ print("\nExiting spool mode.")
347
+ break
348
+
349
+ context['output'] = "Exited spool mode."
350
+ context['messages'] = messages
@@ -0,0 +1,20 @@
1
+ jinx_name: sql
2
+ description: Execute queries on the ~/npcsh_history.db to pull data. The database contains only information about conversations and other user-provided data. It does not store any information about individual files.
3
+ inputs:
4
+ - sql_query: ""
5
+
6
+ steps:
7
+ - name: execute_sql
8
+ engine: python
9
+ code: |
10
+ import pandas as pd
11
+
12
+ query = context.get('sql_query', '').strip()
13
+ if not query:
14
+ context['output'] = "Usage: /sql <query>"
15
+ else:
16
+ try:
17
+ df = pd.read_sql_query(query, npc.db_conn)
18
+ context['output'] = df.to_string()
19
+ except Exception as e:
20
+ context['output'] = "SQL Error: " + str(e)
@@ -1,4 +1,4 @@
1
- jinx_name: studio.switch_npc
1
+ jinx_name: studio_switch_npc
2
2
  description: Switch the active NPC in a chat pane.
3
3
  inputs:
4
4
  - paneId: "active"
@@ -1,4 +1,4 @@
1
- jinx_name: studio.switch_tab
1
+ jinx_name: studio_switch_tab
2
2
  description: Switch to a specific tab in a pane.
3
3
  inputs:
4
4
  - paneId: "active"