npcsh 1.1.16__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 (217) hide show
  1. npcsh/_state.py +138 -100
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +28 -0
  4. npcsh/benchmark/npcsh_agent.py +296 -0
  5. npcsh/benchmark/runner.py +611 -0
  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 +146 -0
  18. npcsh/npc_team/jinxs/bin/nql.jinx +7 -7
  19. npcsh/npc_team/jinxs/bin/roll.jinx +20 -23
  20. npcsh/npc_team/jinxs/bin/sample.jinx +6 -7
  21. npcsh/npc_team/jinxs/bin/sync.jinx +6 -6
  22. npcsh/npc_team/jinxs/bin/vixynt.jinx +8 -8
  23. npcsh/npc_team/jinxs/incognide/add_tab.jinx +11 -0
  24. npcsh/npc_team/jinxs/incognide/close_pane.jinx +9 -0
  25. npcsh/npc_team/jinxs/incognide/close_tab.jinx +10 -0
  26. npcsh/npc_team/jinxs/incognide/confirm.jinx +10 -0
  27. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +9 -0
  28. npcsh/npc_team/jinxs/{npc_studio/npc-studio.jinx → incognide/incognide.jinx} +2 -2
  29. npcsh/npc_team/jinxs/incognide/list_panes.jinx +8 -0
  30. npcsh/npc_team/jinxs/incognide/navigate.jinx +10 -0
  31. npcsh/npc_team/jinxs/incognide/notify.jinx +10 -0
  32. npcsh/npc_team/jinxs/incognide/open_pane.jinx +13 -0
  33. npcsh/npc_team/jinxs/incognide/read_pane.jinx +9 -0
  34. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +10 -0
  35. npcsh/npc_team/jinxs/incognide/send_message.jinx +10 -0
  36. npcsh/npc_team/jinxs/incognide/split_pane.jinx +12 -0
  37. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +10 -0
  38. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +10 -0
  39. npcsh/npc_team/jinxs/incognide/write_file.jinx +11 -0
  40. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +9 -0
  41. npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +4 -4
  42. npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +1 -1
  43. npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +2 -2
  44. npcsh/npc_team/jinxs/lib/computer_use/click.jinx +2 -2
  45. npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +1 -1
  46. npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +1 -1
  47. npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +1 -1
  48. npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +2 -2
  49. npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +1 -1
  50. npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +1 -1
  51. npcsh/npc_team/jinxs/lib/core/chat.jinx +4 -4
  52. npcsh/npc_team/jinxs/lib/core/cmd.jinx +4 -4
  53. npcsh/npc_team/jinxs/lib/core/compress.jinx +8 -8
  54. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +3 -0
  55. npcsh/npc_team/jinxs/lib/core/ots.jinx +7 -7
  56. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +348 -0
  57. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +339 -0
  58. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +418 -0
  59. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  60. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +388 -0
  61. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +283 -0
  62. npcsh/npc_team/jinxs/lib/core/search.jinx +52 -129
  63. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  64. npcsh/npc_team/jinxs/lib/core/sleep.jinx +29 -18
  65. npcsh/npc_team/jinxs/lib/core/sql.jinx +15 -11
  66. npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +7 -7
  67. npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +8 -9
  68. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +389 -78
  69. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +373 -56
  70. npcsh/npc_team/jinxs/lib/utils/build.jinx +5 -5
  71. npcsh/npc_team/jinxs/lib/utils/compile.jinx +2 -2
  72. npcsh/npc_team/jinxs/lib/utils/help.jinx +1 -1
  73. npcsh/npc_team/jinxs/lib/utils/init.jinx +5 -5
  74. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +300 -145
  75. npcsh/npc_team/jinxs/lib/utils/serve.jinx +2 -2
  76. npcsh/npc_team/jinxs/lib/utils/set.jinx +2 -2
  77. npcsh/npc_team/jinxs/lib/utils/switch.jinx +3 -3
  78. npcsh/npc_team/jinxs/lib/utils/switches.jinx +1 -1
  79. npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +2 -2
  80. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  81. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  82. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  83. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  84. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  85. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  86. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  87. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  88. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  89. {npcsh-1.1.16.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/yap.jinx +8 -2
  90. npcsh/npc_team/sibiji.npc +1 -1
  91. npcsh/npcsh.py +87 -46
  92. npcsh/plonk.py +0 -1
  93. npcsh/pti.py +0 -1
  94. npcsh/routes.py +1 -3
  95. npcsh/spool.py +0 -1
  96. npcsh/ui.py +0 -1
  97. npcsh/wander.py +0 -1
  98. npcsh/yap.py +0 -1
  99. npcsh-1.1.18.data/data/npcsh/npc_team/add_tab.jinx +11 -0
  100. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  101. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  102. npcsh-1.1.18.data/data/npcsh/npc_team/benchmark.jinx +146 -0
  103. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +4 -4
  104. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +1 -1
  105. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +5 -5
  106. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +4 -4
  107. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +2 -2
  108. npcsh-1.1.18.data/data/npcsh/npc_team/close_pane.jinx +9 -0
  109. npcsh-1.1.18.data/data/npcsh/npc_team/close_tab.jinx +10 -0
  110. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +4 -4
  111. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +2 -2
  112. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +8 -8
  113. npcsh-1.1.18.data/data/npcsh/npc_team/confirm.jinx +10 -0
  114. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +7 -7
  115. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  116. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  117. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +8 -9
  118. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +3 -0
  119. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  120. npcsh-1.1.18.data/data/npcsh/npc_team/focus_pane.jinx +9 -0
  121. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  122. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +1 -1
  123. npcsh-1.1.16.data/data/npcsh/npc_team/npc-studio.jinx → npcsh-1.1.18.data/data/npcsh/npc_team/incognide.jinx +2 -2
  124. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +5 -5
  125. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  126. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +1 -1
  127. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  128. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +1 -1
  129. npcsh-1.1.18.data/data/npcsh/npc_team/list_panes.jinx +8 -0
  130. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  131. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  132. npcsh-1.1.18.data/data/npcsh/npc_team/navigate.jinx +10 -0
  133. npcsh-1.1.18.data/data/npcsh/npc_team/notify.jinx +10 -0
  134. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +7 -7
  135. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +2 -2
  136. npcsh-1.1.18.data/data/npcsh/npc_team/open_pane.jinx +13 -0
  137. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +7 -7
  138. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  139. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  140. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  141. npcsh-1.1.18.data/data/npcsh/npc_team/read_pane.jinx +9 -0
  142. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  143. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +20 -23
  144. npcsh-1.1.18.data/data/npcsh/npc_team/run_terminal.jinx +10 -0
  145. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +6 -7
  146. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +1 -1
  147. npcsh-1.1.18.data/data/npcsh/npc_team/search.jinx +54 -0
  148. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  149. npcsh-1.1.18.data/data/npcsh/npc_team/send_message.jinx +10 -0
  150. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +2 -2
  151. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +2 -2
  152. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +1 -1
  153. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +1 -1
  154. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +29 -18
  155. npcsh-1.1.18.data/data/npcsh/npc_team/split_pane.jinx +12 -0
  156. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  157. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  158. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +3 -3
  159. npcsh-1.1.18.data/data/npcsh/npc_team/switch_npc.jinx +10 -0
  160. npcsh-1.1.18.data/data/npcsh/npc_team/switch_tab.jinx +10 -0
  161. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +1 -1
  162. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +6 -6
  163. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +2 -2
  164. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +2 -2
  165. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +1 -1
  166. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +8 -8
  167. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +1 -1
  168. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  169. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  170. npcsh-1.1.18.data/data/npcsh/npc_team/write_file.jinx +11 -0
  171. {npcsh/npc_team/jinxs/bin → npcsh-1.1.18.data/data/npcsh/npc_team}/yap.jinx +8 -2
  172. npcsh-1.1.18.data/data/npcsh/npc_team/zen_mode.jinx +9 -0
  173. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/METADATA +99 -7
  174. npcsh-1.1.18.dist-info/RECORD +235 -0
  175. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  176. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +2 -3
  177. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  178. npcsh/npc_team/jinxs/bin/wander.jinx +0 -152
  179. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  180. npcsh-1.1.16.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  181. npcsh-1.1.16.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  182. npcsh-1.1.16.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  183. npcsh-1.1.16.data/data/npcsh/npc_team/search.jinx +0 -131
  184. npcsh-1.1.16.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  185. npcsh-1.1.16.data/data/npcsh/npc_team/spool.jinx +0 -161
  186. npcsh-1.1.16.data/data/npcsh/npc_team/sql.jinx +0 -16
  187. npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +0 -152
  188. npcsh-1.1.16.dist-info/RECORD +0 -170
  189. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  190. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  191. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  192. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  193. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  194. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  195. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  196. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  197. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  198. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  199. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  200. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  201. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  202. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  203. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  204. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  205. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  206. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  207. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  208. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  209. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  210. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  211. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  212. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  213. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  214. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  215. {npcsh-1.1.16.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  216. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  217. {npcsh-1.1.16.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
npcsh/_state.py CHANGED
@@ -1,11 +1,14 @@
1
1
  # Standard library imports
2
2
  import atexit
3
+ import base64
4
+ import os
3
5
  from dataclasses import dataclass, field
4
6
  from datetime import datetime
5
7
  import filecmp
6
8
  import inspect
9
+
7
10
  import logging
8
- import os
11
+
9
12
  from pathlib import Path
10
13
  import platform
11
14
  import re
@@ -16,8 +19,11 @@ import signal
16
19
  import sqlite3
17
20
  import subprocess
18
21
  import sys
22
+ import tempfile
19
23
  import time
20
24
  import textwrap
25
+ import readline
26
+ import json
21
27
  from typing import Dict, List, Any, Tuple, Union, Optional, Callable
22
28
  import yaml
23
29
 
@@ -40,9 +46,9 @@ try:
40
46
  import pty
41
47
  import tty
42
48
  import termios
43
- import readline
49
+
44
50
  except ImportError:
45
- readline = None
51
+
46
52
  pty = None
47
53
  tty = None
48
54
  termios = None
@@ -53,15 +59,21 @@ try:
53
59
  except ImportError:
54
60
  chromadb = None
55
61
 
62
+ try:
63
+ import ollama
64
+ except ImportError:
65
+ ollama = None
66
+
56
67
  # Third-party imports
57
- from colorama import Fore, Back, Style
68
+ from colorama import Style
58
69
  from litellm import RateLimitError
70
+ import numpy as np
59
71
  from termcolor import colored
60
72
 
61
73
  # npcpy imports
62
74
  from npcpy.data.load import load_file_contents
63
75
  from npcpy.data.web import search_web
64
- from npcpy.gen.embeddings import get_embeddings
76
+
65
77
  from npcpy.llm_funcs import (
66
78
  check_llm_command,
67
79
  get_llm_response,
@@ -74,25 +86,25 @@ from npcpy.memory.command_history import (
74
86
  save_conversation_message,
75
87
  load_kg_from_db,
76
88
  save_kg_to_db,
89
+ format_memory_context,
77
90
  )
78
91
  from npcpy.memory.knowledge_graph import kg_evolve_incremental
79
92
  from npcpy.memory.search import execute_rag_command, execute_brainblast_command
80
- from npcpy.npc_compiler import NPC, Team, load_jinxs_from_directory, build_jinx_tool_catalog
93
+ from npcpy.npc_compiler import NPC, Team, build_jinx_tool_catalog
81
94
  from npcpy.npc_sysenv import (
82
95
  print_and_process_stream_with_markdown,
83
96
  render_markdown,
84
97
  get_model_and_provider,
85
98
  get_locally_available_models,
86
- lookup_provider
99
+
87
100
  )
88
101
  from npcpy.tools import auto_tools
102
+ from npcpy.gen.embeddings import get_embeddings
89
103
 
90
104
  # Local module imports
91
105
  from .config import (
92
- VERSION,
93
106
  DEFAULT_NPC_TEAM_PATH,
94
107
  PROJECT_NPC_TEAM_PATH,
95
- HISTORY_DB_DEFAULT_PATH,
96
108
  READLINE_HISTORY_FILE,
97
109
  NPCSH_CHAT_MODEL,
98
110
  NPCSH_CHAT_PROVIDER,
@@ -156,6 +168,9 @@ class ShellState:
156
168
  video_gen_provider: str = NPCSH_VIDEO_GEN_PROVIDER
157
169
  current_mode: str = NPCSH_DEFAULT_MODE
158
170
  build_kg: bool = NPCSH_BUILD_KG
171
+ kg_link_facts: bool = False # Link facts to concepts (requires LLM calls)
172
+ kg_link_concepts: bool = False # Link concepts to concepts (requires LLM calls)
173
+ kg_link_facts_facts: bool = False # Link facts to facts (requires LLM calls)
159
174
  api_key: Optional[str] = None
160
175
  api_url: Optional[str] = NPCSH_API_URL
161
176
  current_path: str = field(default_factory=os.getcwd)
@@ -303,6 +318,33 @@ def set_npcsh_config_value(key: str, value: str):
303
318
  }
304
319
  if env_key in field_map:
305
320
  setattr(ShellState, field_map[env_key], parsed_val)
321
+
322
+ # Persist to ~/.npcshrc
323
+ npcshrc_path = os.path.expanduser("~/.npcshrc")
324
+ try:
325
+ existing_lines = []
326
+ if os.path.exists(npcshrc_path):
327
+ with open(npcshrc_path, 'r') as f:
328
+ existing_lines = f.readlines()
329
+
330
+ # Update or add the export line
331
+ export_line = f"export {env_key}=\"{value}\"\n"
332
+ found = False
333
+ for i, line in enumerate(existing_lines):
334
+ if line.strip().startswith(f"export {env_key}="):
335
+ existing_lines[i] = export_line
336
+ found = True
337
+ break
338
+
339
+ if not found:
340
+ existing_lines.append(export_line)
341
+
342
+ with open(npcshrc_path, 'w') as f:
343
+ f.writelines(existing_lines)
344
+ except Exception as e:
345
+ print(f"Warning: Could not persist config to {npcshrc_path}: {e}")
346
+
347
+
306
348
  def get_npc_path(npc_name: str, db_path: str) -> str:
307
349
  project_npc_team_dir = os.path.abspath("./npc_team")
308
350
  project_npc_path = os.path.join(project_npc_team_dir, f"{npc_name}.npc")
@@ -317,7 +359,7 @@ def get_npc_path(npc_name: str, db_path: str) -> str:
317
359
  if result:
318
360
  return result[0]
319
361
 
320
- except Exception as e:
362
+ except Exception:
321
363
  try:
322
364
  with sqlite3.connect(db_path) as conn:
323
365
  cursor = conn.cursor()
@@ -351,8 +393,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
351
393
  None
352
394
  """
353
395
 
354
- if is_npcsh_initialized():
355
- return
396
+ already_initialized = is_npcsh_initialized()
356
397
 
357
398
  conn = sqlite3.connect(db_path)
358
399
  cursor = conn.cursor()
@@ -425,10 +466,10 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
425
466
  old_package_jinxs = set()
426
467
  if os.path.exists(manifest_path):
427
468
  try:
428
- import json
469
+
429
470
  with open(manifest_path, 'r') as f:
430
471
  old_package_jinxs = set(json.load(f).get('jinxs', []))
431
- except:
472
+ except Exception:
432
473
  pass
433
474
 
434
475
  # Track current package jinxs
@@ -481,7 +522,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
481
522
 
482
523
  # Save updated manifest
483
524
  try:
484
- import json
525
+
485
526
  with open(manifest_path, 'w') as f:
486
527
  json.dump({'jinxs': list(current_package_jinxs), 'updated': str(__import__('datetime').datetime.now())}, f, indent=2)
487
528
  except Exception as e:
@@ -508,8 +549,10 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
508
549
  print(f"Copied template {file} to {destination_template_path}")
509
550
  conn.commit()
510
551
  conn.close()
511
- set_npcsh_initialized()
512
- add_npcshrc_to_shell_config()
552
+
553
+ if not already_initialized:
554
+ set_npcsh_initialized()
555
+ add_npcshrc_to_shell_config()
513
556
 
514
557
 
515
558
  def get_shell_config_file() -> str:
@@ -560,9 +603,6 @@ def get_relevant_memories(
560
603
  max_memories: int = 10,
561
604
  state: Optional[ShellState] = None
562
605
  ) -> List[Dict]:
563
-
564
- engine = command_history.engine
565
-
566
606
  all_memories = command_history.get_memories_for_scope(
567
607
  npc=npc_name,
568
608
  team=team_name,
@@ -587,7 +627,7 @@ def get_relevant_memories(
587
627
 
588
628
  if state and state.embedding_model and state.embedding_provider:
589
629
  try:
590
- from npcpy.gen.embeddings import get_embeddings
630
+
591
631
 
592
632
  search_text = query if query else "recent context"
593
633
  query_embedding = get_embeddings(
@@ -605,7 +645,7 @@ def get_relevant_memories(
605
645
  state.embedding_provider
606
646
  )
607
647
 
608
- import numpy as np
648
+
609
649
  similarities = []
610
650
  for mem_emb in memory_embeddings:
611
651
  similarity = np.dot(query_embedding, mem_emb) / (
@@ -815,7 +855,6 @@ BASH_COMMANDS = [
815
855
  "command",
816
856
  "compgen",
817
857
  "complete",
818
- "continue",
819
858
  "declare",
820
859
  "dirs",
821
860
  "disown",
@@ -1282,7 +1321,7 @@ def get_setting_windows(key, default=None):
1282
1321
 
1283
1322
 
1284
1323
  def setup_readline() -> str:
1285
- import readline
1324
+
1286
1325
  if readline is None:
1287
1326
  return None
1288
1327
  try:
@@ -1428,7 +1467,7 @@ def make_completer(shell_state: ShellState, router: Any):
1428
1467
  else:
1429
1468
  return None # readline expects None when no more completions
1430
1469
 
1431
- except Exception as e:
1470
+ except Exception:
1432
1471
  # Using completion_logger for internal debugging, not printing to stdout for user.
1433
1472
  # completion_logger.error(f"Exception in completion: {e}", exc_info=True)
1434
1473
  return None
@@ -1588,7 +1627,7 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1588
1627
  try:
1589
1628
  import termios
1590
1629
  import tty
1591
- import readline
1630
+
1592
1631
  except ImportError:
1593
1632
  return input(prompt)
1594
1633
 
@@ -1624,7 +1663,7 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1624
1663
  try:
1625
1664
  import shutil
1626
1665
  term_width = shutil.get_terminal_size().columns
1627
- except:
1666
+ except json.JSONDecodeError:
1628
1667
  term_width = 80
1629
1668
 
1630
1669
  def draw():
@@ -1637,7 +1676,6 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1637
1676
  sys.stdout.write('\r')
1638
1677
  # Move up for each wrapped line we're on
1639
1678
  cursor_total = prompt_visible_len + pos
1640
- cursor_line = cursor_total // term_width
1641
1679
  # Go up to the first line of input
1642
1680
  for _ in range(num_lines - 1):
1643
1681
  sys.stdout.write('\033[A')
@@ -1720,7 +1758,7 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1720
1758
  # Check if this looks like binary/image data
1721
1759
  # Image signatures: PNG (\x89PNG), JPEG (\xff\xd8\xff), GIF (GIF8), BMP (BM)
1722
1760
  # Also check for high ratio of non-printable chars
1723
- is_binary = False
1761
+
1724
1762
  if len(paste_buffer) > 4:
1725
1763
  # Check for common image magic bytes
1726
1764
  if paste_buffer[:4] == '\x89PNG' or paste_buffer[:8] == '\x89PNG\r\n\x1a\n':
@@ -1741,8 +1779,8 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1741
1779
 
1742
1780
  if is_binary:
1743
1781
  # Save image data to temp file
1744
- import tempfile
1745
- import os
1782
+
1783
+
1746
1784
  try:
1747
1785
  # Determine extension from magic bytes
1748
1786
  ext = '.bin'
@@ -1765,14 +1803,14 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1765
1803
  with os.fdopen(fd, 'wb') as f:
1766
1804
  if paste_buffer.startswith('data:image/'):
1767
1805
  # Decode base64 data URL
1768
- import base64
1806
+
1769
1807
  _, data = paste_buffer.split(',', 1)
1770
1808
  f.write(base64.b64decode(data))
1771
1809
  else:
1772
1810
  f.write(paste_buffer.encode('latin-1'))
1773
1811
  pasted_content = temp_path # Store path to image
1774
1812
  placeholder = f"[pasted image: {temp_path}]"
1775
- except:
1813
+ except Exception:
1776
1814
  pasted_content = None
1777
1815
  placeholder = "[pasted image: failed to save]"
1778
1816
  else:
@@ -1970,7 +2008,7 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1970
2008
  sys.stdout.flush()
1971
2009
  else:
1972
2010
  pass # No tool call to show
1973
- except:
2011
+ except Exception:
1974
2012
  pass
1975
2013
 
1976
2014
  elif c and ord(c) >= 32: # Printable
@@ -1999,7 +2037,7 @@ def _get_slash_hints(state, router, prefix='/') -> str:
1999
2037
  try:
2000
2038
  import shutil
2001
2039
  term_width = shutil.get_terminal_size().columns
2002
- except:
2040
+ except Exception:
2003
2041
  term_width = 80
2004
2042
 
2005
2043
  # Build hint string that fits in terminal
@@ -2148,44 +2186,20 @@ def wrap_text(text: str, width: int = 80) -> str:
2148
2186
 
2149
2187
 
2150
2188
 
2151
- def setup_readline() -> str:
2152
- """Setup readline with history and completion"""
2153
- try:
2154
- readline.read_history_file(READLINE_HISTORY_FILE)
2155
- readline.set_history_length(1000)
2156
-
2157
-
2158
- readline.parse_and_bind("tab: complete")
2159
-
2160
- readline.parse_and_bind("set enable-bracketed-paste on")
2161
- readline.parse_and_bind(r'"\C-r": reverse-search-history')
2162
- readline.parse_and_bind(r'"\C-e": end-of-line')
2163
- readline.parse_and_bind(r'"\C-a": beginning-of-line')
2164
-
2165
- return READLINE_HISTORY_FILE
2166
-
2167
- except FileNotFoundError:
2168
- pass
2169
- except OSError as e:
2170
- print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
2171
-
2172
2189
 
2173
- def save_readline_history():
2174
- try:
2175
- readline.write_history_file(READLINE_HISTORY_FILE)
2176
- except OSError as e:
2177
- print(f"Warning: Could not write readline history file {READLINE_HISTORY_FILE}: {e}")
2178
2190
 
2179
2191
  def store_command_embeddings(command: str, output: Any, state: ShellState):
2180
2192
  if not chroma_client or not state.embedding_model or not state.embedding_provider:
2181
- if not chroma_client: print("Warning: ChromaDB client not available for embeddings.", file=sys.stderr)
2193
+ if not chroma_client:
2194
+ print("Warning: ChromaDB client not available for embeddings.", file=sys.stderr)
2182
2195
  return
2183
2196
  if not command and not output:
2184
2197
  return
2185
2198
 
2186
2199
  try:
2187
2200
  output_str = str(output) if output else ""
2188
- if not command and not output_str: return
2201
+ if not command and not output_str:
2202
+ return
2189
2203
 
2190
2204
  texts_to_embed = [command, output_str]
2191
2205
 
@@ -2357,10 +2371,7 @@ def _ollama_supports_tools(model: str) -> Optional[bool]:
2357
2371
  Best-effort check for tool-call support on an Ollama model by inspecting its template/metadata.
2358
2372
  Mirrors the lightweight check used in the Flask serve path.
2359
2373
  """
2360
- try:
2361
- import ollama # Local import to avoid hard dependency when Ollama isn't installed
2362
- except Exception:
2363
- return None
2374
+
2364
2375
 
2365
2376
  try:
2366
2377
  details = ollama.show(model)
@@ -2467,7 +2478,7 @@ def wrap_tool_with_display(tool_name: str, tool_func: Callable, state: ShellStat
2467
2478
  print(colored(f" ⚡ {tool_name}", "cyan") + colored(f" {args_display}", "white", attrs=["dark"]), end="", flush=True)
2468
2479
  else:
2469
2480
  print(colored(f" ⚡ {tool_name}", "cyan"), end="", flush=True)
2470
- except:
2481
+ except Exception:
2471
2482
  pass
2472
2483
 
2473
2484
  # Execute tool
@@ -2483,14 +2494,14 @@ def wrap_tool_with_display(tool_name: str, tool_func: Callable, state: ShellStat
2483
2494
  result_preview = result_preview[:200] + "..."
2484
2495
  if result_preview and result_preview not in ('None', '', '{}', '[]'):
2485
2496
  print(colored(f" → {result_preview}", "white", attrs=["dark"]), flush=True)
2486
- except:
2497
+ except Exception:
2487
2498
  pass
2488
2499
  return result
2489
2500
  except Exception as e:
2490
2501
  if log_level != "silent":
2491
2502
  try:
2492
2503
  print(colored(f" ✗ {str(e)[:100]}", "red"), flush=True)
2493
- except:
2504
+ except Exception as e:
2494
2505
  pass
2495
2506
  raise
2496
2507
  return wrapped
@@ -2540,8 +2551,19 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
2540
2551
  if not jinja_env_for_jinx and state.team and isinstance(state.team, Team):
2541
2552
  jinja_env_for_jinx = getattr(state.team, "jinja_env", None)
2542
2553
 
2554
+ jinx_globals = {
2555
+ "state": state,
2556
+ "CommandHistory": CommandHistory,
2557
+ "load_kg_from_db": load_kg_from_db,
2558
+ "execute_rag_command": execute_rag_command,
2559
+ "execute_brainblast_command": execute_brainblast_command,
2560
+ "load_file_contents": load_file_contents,
2561
+ "search_web": search_web,
2562
+ "get_relevant_memories": get_relevant_memories,
2563
+ }
2564
+
2543
2565
  for name, jinx_obj in aggregated_jinxs.items():
2544
- def _make_runner(jinx=jinx_obj, jinja_env=jinja_env_for_jinx, tool_name=name):
2566
+ def _make_runner(jinx=jinx_obj, jinja_env=jinja_env_for_jinx, tool_name=name, extras=jinx_globals):
2545
2567
  def runner(**kwargs):
2546
2568
  input_values = kwargs if isinstance(kwargs, dict) else {}
2547
2569
  try:
@@ -2549,7 +2571,7 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
2549
2571
  input_values=input_values,
2550
2572
  npc=npc_obj,
2551
2573
  messages=state.messages,
2552
- extra_globals={"state": state},
2574
+ extra_globals=extras,
2553
2575
  jinja_env=jinja_env
2554
2576
  )
2555
2577
  return ctx.get("output", ctx)
@@ -2630,10 +2652,10 @@ def should_skip_kg_processing(user_input: str, assistant_output: str) -> bool:
2630
2652
 
2631
2653
  return False
2632
2654
 
2633
- def execute_slash_command(command: str,
2634
- stdin_input: Optional[str],
2635
- state: ShellState,
2636
- stream: bool,
2655
+ def execute_slash_command(command: str,
2656
+ stdin_input: Optional[str],
2657
+ state: ShellState,
2658
+ stream: bool,
2637
2659
  router) -> Tuple[ShellState, Any]:
2638
2660
  """Executes slash commands using the router."""
2639
2661
  try:
@@ -2641,7 +2663,13 @@ def execute_slash_command(command: str,
2641
2663
  except ValueError:
2642
2664
  all_command_parts = command.split()
2643
2665
  command_name = all_command_parts[0].lstrip('/')
2666
+
2667
+ # --- QUIT/EXIT HANDLING ---
2668
+ if command_name in ['quit', 'exit', 'q']:
2644
2669
 
2670
+ print("Goodbye!")
2671
+ sys.exit(0)
2672
+
2645
2673
  # --- NPC SWITCHING LOGIC ---
2646
2674
  if command_name in ['n', 'npc']:
2647
2675
  npc_to_switch_to = all_command_parts[1] if len(all_command_parts) > 1 else None
@@ -2902,9 +2930,6 @@ def process_pipeline_command(
2902
2930
  "tools": tools_for_llm,
2903
2931
  "tool_map": tool_exec_map,
2904
2932
  }
2905
- # Only add tool_choice for providers that support it (not gemini)
2906
- is_gemini = (exec_provider and "gemini" in exec_provider.lower()) or \
2907
- (exec_model and "gemini" in exec_model.lower())
2908
2933
  llm_kwargs["tool_choice"] = 'auto'
2909
2934
 
2910
2935
  # Agent loop: keep calling LLM until it stops making tool calls
@@ -2945,14 +2970,17 @@ def process_pipeline_command(
2945
2970
  tool_name = msg.get("name", "tool")
2946
2971
  tool_content = msg.get("content", "")
2947
2972
  if tool_content and tool_content.strip():
2973
+ # Decode escaped newlines if present
2974
+ if isinstance(tool_content, str):
2975
+ tool_content = tool_content.replace('\\n', '\n').replace('\\t', '\t')
2948
2976
  print(colored(f"\n⚡ {tool_name}:", "cyan"))
2949
2977
  lines = tool_content.split('\n')
2950
2978
  if len(lines) > 50:
2951
- print('\n'.join(lines[:25]))
2979
+ render_markdown('\n'.join(lines[:25]))
2952
2980
  print(colored(f"\n... ({len(lines) - 50} lines hidden) ...\n", "white", attrs=["dark"]))
2953
- print('\n'.join(lines[-25:]))
2981
+ render_markdown('\n'.join(lines[-25:]))
2954
2982
  else:
2955
- print(tool_content)
2983
+ render_markdown(tool_content)
2956
2984
 
2957
2985
  # Check if LLM made tool calls - if not, it's done
2958
2986
  tool_calls_made = isinstance(llm_result, dict) and llm_result.get("tool_calls")
@@ -3090,7 +3118,7 @@ def _delegate_to_npc(state: ShellState, npc_name: str, command: str, delegation_
3090
3118
  MAX_DELEGATION_DEPTH = 1 # Only allow one level of delegation
3091
3119
 
3092
3120
  if delegation_depth > MAX_DELEGATION_DEPTH:
3093
- return state, {'output': f"⚠ Maximum delegation depth reached."}
3121
+ return state, {'output': "⚠ Maximum delegation depth reached."}
3094
3122
 
3095
3123
  if not state.team or not hasattr(state.team, 'npcs') or npc_name not in state.team.npcs:
3096
3124
  return state, {'output': f"⚠ NPC '{npc_name}' not found in team"}
@@ -3296,7 +3324,7 @@ def execute_command(
3296
3324
  )
3297
3325
  )
3298
3326
  stdin_for_next = full_stream_output
3299
- except:
3327
+ except Exception:
3300
3328
  if output is not None:
3301
3329
  try:
3302
3330
  stdin_for_next = str(output)
@@ -3398,7 +3426,7 @@ def execute_command(
3398
3426
  def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3399
3427
  setup_npcsh_config()
3400
3428
 
3401
- db_path = os.getenv("NPCSH_DB_PATH", HISTORY_DB_DEFAULT_PATH)
3429
+ db_path = NPCSH_DB_PATH
3402
3430
  db_path = os.path.expanduser(db_path)
3403
3431
  os.makedirs(os.path.dirname(db_path), exist_ok=True)
3404
3432
  command_history = CommandHistory(db_path)
@@ -3409,11 +3437,11 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3409
3437
  print("NPCSH initialization complete. Restart or source ~/.npcshrc.")
3410
3438
 
3411
3439
  try:
3412
- history_file = setup_readline()
3440
+ setup_readline()
3413
3441
  atexit.register(save_readline_history)
3414
3442
  atexit.register(command_history.close)
3415
- except:
3416
- pass
3443
+ except OSError as e:
3444
+ print(f"Warning: Failed to setup readline history: {e}", file=sys.stderr)
3417
3445
 
3418
3446
  project_team_path = os.path.abspath(PROJECT_NPC_TEAM_PATH)
3419
3447
  global_team_path = os.path.expanduser(DEFAULT_NPC_TEAM_PATH)
@@ -3422,7 +3450,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3422
3450
  default_forenpc_name = None
3423
3451
  global_team_path = os.path.expanduser(DEFAULT_NPC_TEAM_PATH)
3424
3452
  if not os.path.exists(global_team_path):
3425
- print(f"Global NPC team directory doesn't exist. Initializing...")
3453
+ print("Global NPC team directory doesn't exist. Initializing...")
3426
3454
  initialize_base_npcs_if_needed(db_path)
3427
3455
  if os.path.exists(project_team_path):
3428
3456
  team_dir = project_team_path
@@ -3642,6 +3670,10 @@ def process_result(
3642
3670
  final_output_str = None
3643
3671
 
3644
3672
  # FIX: Handle dict output properly
3673
+ msg_input_tokens = None
3674
+ msg_output_tokens = None
3675
+ msg_cost = None
3676
+
3645
3677
  if isinstance(output, dict):
3646
3678
  # Use None-safe check to not skip empty strings
3647
3679
  output_content = output.get('output') if 'output' in output else output.get('response')
@@ -3651,15 +3683,18 @@ def process_result(
3651
3683
  # Accumulate token usage if available
3652
3684
  if 'usage' in output:
3653
3685
  usage = output['usage']
3654
- result_state.session_input_tokens += usage.get('input_tokens', 0)
3655
- result_state.session_output_tokens += usage.get('output_tokens', 0)
3686
+ msg_input_tokens = usage.get('input_tokens', 0)
3687
+ msg_output_tokens = usage.get('output_tokens', 0)
3688
+ result_state.session_input_tokens += msg_input_tokens
3689
+ result_state.session_output_tokens += msg_output_tokens
3656
3690
  # Calculate cost
3657
3691
  from npcpy.gen.response import calculate_cost
3658
- result_state.session_cost_usd += calculate_cost(
3692
+ msg_cost = calculate_cost(
3659
3693
  model_for_stream,
3660
- usage.get('input_tokens', 0),
3661
- usage.get('output_tokens', 0)
3694
+ msg_input_tokens,
3695
+ msg_output_tokens
3662
3696
  )
3697
+ result_state.session_cost_usd += msg_cost
3663
3698
 
3664
3699
  # If output_content is still a dict, convert to string
3665
3700
  if isinstance(output_content, dict):
@@ -3721,6 +3756,9 @@ def process_result(
3721
3756
  provider=active_npc.provider,
3722
3757
  npc=npc_name,
3723
3758
  team=team_name,
3759
+ input_tokens=msg_input_tokens,
3760
+ output_tokens=msg_output_tokens,
3761
+ cost=msg_cost,
3724
3762
  )
3725
3763
 
3726
3764
  result_state.turn_count += 1
@@ -3829,15 +3867,15 @@ def process_result(
3829
3867
  result_state.current_path
3830
3868
  )
3831
3869
  evolved_npc_kg, _ = kg_evolve_incremental(
3832
- existing_kg=npc_kg,
3870
+ existing_kg=npc_kg,
3833
3871
  new_facts=approved_facts,
3834
- model=active_npc.model,
3835
- provider=active_npc.provider,
3872
+ model=active_npc.model,
3873
+ provider=active_npc.provider,
3836
3874
  npc=active_npc,
3837
3875
  get_concepts=True,
3838
- link_concepts_facts=False,
3839
- link_concepts_concepts=False,
3840
- link_facts_facts=False,
3876
+ link_concepts_facts=result_state.kg_link_facts,
3877
+ link_concepts_concepts=result_state.kg_link_concepts,
3878
+ link_facts_facts=result_state.kg_link_facts_facts,
3841
3879
  )
3842
3880
  save_kg_to_db(
3843
3881
  engine,
npcsh/alicanto.py CHANGED
@@ -4,7 +4,7 @@ alicanto - Deep research mode CLI entry point
4
4
  This is a thin wrapper that executes the alicanto.jinx through the jinx mechanism.
5
5
  """
6
6
  import argparse
7
- import os
7
+
8
8
  import sys
9
9
 
10
10
  from npcsh._state import setup_shell
@@ -30,7 +30,7 @@ def main():
30
30
  sys.exit(1)
31
31
 
32
32
  # Setup shell to get team and default NPC
33
- command_history, team, default_npc = setup_shell()
33
+ _, team, default_npc = setup_shell()
34
34
 
35
35
  if not team or "alicanto" not in team.jinxs_dict:
36
36
  print("Error: alicanto jinx not found. Ensure npc_team/jinxs/modes/alicanto.jinx exists.")
@@ -0,0 +1,28 @@
1
+ """
2
+ npcsh benchmark integration for Terminal-Bench.
3
+
4
+ This module provides integration with Terminal-Bench (tbench.ai) for benchmarking
5
+ npcsh against standardized terminal/CLI agent evaluation tasks.
6
+
7
+ Usage:
8
+ # Install terminal-bench
9
+ pip install terminal-bench harbor
10
+
11
+ # Run benchmarks with npcsh
12
+ harbor run -d terminal-bench@2.0 --agent-import-path npcsh.benchmark:NpcshAgent -m anthropic/claude-sonnet-4-20250514
13
+
14
+ # Or use the convenience function
15
+ from npcsh.benchmark import run_benchmark
16
+ run_benchmark(model="claude-sonnet-4-20250514", provider="anthropic")
17
+ """
18
+
19
+ from .runner import run_benchmark, BenchmarkRunner
20
+
21
+ __all__ = ["run_benchmark", "BenchmarkRunner"]
22
+
23
+ # NpcshAgent requires harbor to be installed - import lazily
24
+ try:
25
+ from .npcsh_agent import NpcshAgent
26
+ __all__.append("NpcshAgent")
27
+ except ImportError:
28
+ NpcshAgent = None # Harbor not installed