npcsh 1.1.15__py3-none-any.whl → 1.1.17__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 (176) hide show
  1. npcsh/_state.py +69 -12
  2. npcsh/benchmark/__init__.py +22 -0
  3. npcsh/benchmark/npcsh_agent.py +262 -0
  4. npcsh/benchmark/runner.py +569 -0
  5. npcsh/npc_team/jinxs/bin/benchmark.jinx +146 -0
  6. npcsh/npc_team/jinxs/bin/nql.jinx +7 -7
  7. npcsh/npc_team/jinxs/bin/roll.jinx +20 -23
  8. npcsh/npc_team/jinxs/bin/sample.jinx +6 -7
  9. npcsh/npc_team/jinxs/bin/spool.jinx +4 -4
  10. npcsh/npc_team/jinxs/bin/sync.jinx +6 -6
  11. npcsh/npc_team/jinxs/bin/vixynt.jinx +8 -8
  12. npcsh/npc_team/jinxs/bin/wander.jinx +109 -19
  13. npcsh/npc_team/jinxs/bin/yap.jinx +5 -5
  14. npcsh/npc_team/jinxs/incognide/add_tab.jinx +11 -0
  15. npcsh/npc_team/jinxs/incognide/close_pane.jinx +9 -0
  16. npcsh/npc_team/jinxs/incognide/close_tab.jinx +10 -0
  17. npcsh/npc_team/jinxs/incognide/confirm.jinx +10 -0
  18. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +9 -0
  19. npcsh/npc_team/jinxs/{npc_studio/npc-studio.jinx → incognide/incognide.jinx} +2 -2
  20. npcsh/npc_team/jinxs/incognide/list_panes.jinx +8 -0
  21. npcsh/npc_team/jinxs/incognide/navigate.jinx +10 -0
  22. npcsh/npc_team/jinxs/incognide/notify.jinx +10 -0
  23. npcsh/npc_team/jinxs/incognide/open_pane.jinx +13 -0
  24. npcsh/npc_team/jinxs/incognide/read_pane.jinx +9 -0
  25. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +10 -0
  26. npcsh/npc_team/jinxs/incognide/send_message.jinx +10 -0
  27. npcsh/npc_team/jinxs/incognide/split_pane.jinx +12 -0
  28. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +10 -0
  29. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +10 -0
  30. npcsh/npc_team/jinxs/incognide/write_file.jinx +11 -0
  31. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +9 -0
  32. npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +4 -4
  33. npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +1 -1
  34. npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +2 -2
  35. npcsh/npc_team/jinxs/lib/computer_use/click.jinx +2 -2
  36. npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +1 -1
  37. npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +1 -1
  38. npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +1 -1
  39. npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +2 -2
  40. npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +1 -1
  41. npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +1 -1
  42. npcsh/npc_team/jinxs/lib/core/chat.jinx +4 -4
  43. npcsh/npc_team/jinxs/lib/core/cmd.jinx +4 -4
  44. npcsh/npc_team/jinxs/lib/core/compress.jinx +8 -8
  45. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +3 -0
  46. npcsh/npc_team/jinxs/lib/core/ots.jinx +7 -7
  47. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +44 -0
  48. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +94 -0
  49. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +96 -0
  50. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +80 -0
  51. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +51 -0
  52. npcsh/npc_team/jinxs/lib/core/search.jinx +52 -129
  53. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  54. npcsh/npc_team/jinxs/lib/core/sleep.jinx +7 -7
  55. npcsh/npc_team/jinxs/lib/core/sql.jinx +7 -7
  56. npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +7 -7
  57. npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +8 -9
  58. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +2 -2
  59. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +3 -3
  60. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +2 -2
  61. npcsh/npc_team/jinxs/lib/utils/build.jinx +5 -5
  62. npcsh/npc_team/jinxs/lib/utils/compile.jinx +2 -2
  63. npcsh/npc_team/jinxs/lib/utils/help.jinx +1 -1
  64. npcsh/npc_team/jinxs/lib/utils/init.jinx +5 -5
  65. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +1 -1
  66. npcsh/npc_team/jinxs/lib/utils/serve.jinx +2 -2
  67. npcsh/npc_team/jinxs/lib/utils/set.jinx +2 -2
  68. npcsh/npc_team/jinxs/lib/utils/switch.jinx +3 -3
  69. npcsh/npc_team/jinxs/lib/utils/switches.jinx +1 -1
  70. npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +2 -2
  71. npcsh/npc_team/sibiji.npc +1 -1
  72. npcsh/npcsh.py +81 -43
  73. npcsh-1.1.17.data/data/npcsh/npc_team/add_tab.jinx +11 -0
  74. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/arxiv.jinx +2 -2
  75. npcsh-1.1.17.data/data/npcsh/npc_team/benchmark.jinx +146 -0
  76. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/browser_action.jinx +4 -4
  77. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/browser_screenshot.jinx +1 -1
  78. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/build.jinx +5 -5
  79. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/chat.jinx +4 -4
  80. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/click.jinx +2 -2
  81. npcsh-1.1.17.data/data/npcsh/npc_team/close_pane.jinx +9 -0
  82. npcsh-1.1.17.data/data/npcsh/npc_team/close_tab.jinx +10 -0
  83. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/cmd.jinx +4 -4
  84. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/compile.jinx +2 -2
  85. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/compress.jinx +8 -8
  86. npcsh-1.1.17.data/data/npcsh/npc_team/confirm.jinx +10 -0
  87. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/convene.jinx +7 -7
  88. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +44 -0
  89. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/delegate.jinx +8 -9
  90. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/edit_file.jinx +3 -0
  91. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +94 -0
  92. npcsh-1.1.17.data/data/npcsh/npc_team/focus_pane.jinx +9 -0
  93. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/help.jinx +1 -1
  94. npcsh-1.1.15.data/data/npcsh/npc_team/npc-studio.jinx → npcsh-1.1.17.data/data/npcsh/npc_team/incognide.jinx +2 -2
  95. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/init.jinx +5 -5
  96. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/jinxs.jinx +1 -1
  97. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/key_press.jinx +1 -1
  98. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +96 -0
  99. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/launch_app.jinx +1 -1
  100. npcsh-1.1.17.data/data/npcsh/npc_team/list_panes.jinx +8 -0
  101. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +80 -0
  102. npcsh-1.1.17.data/data/npcsh/npc_team/navigate.jinx +10 -0
  103. npcsh-1.1.17.data/data/npcsh/npc_team/notify.jinx +10 -0
  104. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/nql.jinx +7 -7
  105. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/open_browser.jinx +2 -2
  106. npcsh-1.1.17.data/data/npcsh/npc_team/open_pane.jinx +13 -0
  107. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/ots.jinx +7 -7
  108. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/paper_search.jinx +3 -3
  109. npcsh-1.1.17.data/data/npcsh/npc_team/read_pane.jinx +9 -0
  110. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/roll.jinx +20 -23
  111. npcsh-1.1.17.data/data/npcsh/npc_team/run_terminal.jinx +10 -0
  112. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sample.jinx +6 -7
  113. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/screenshot.jinx +1 -1
  114. npcsh-1.1.17.data/data/npcsh/npc_team/search.jinx +54 -0
  115. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/semantic_scholar.jinx +2 -2
  116. npcsh-1.1.17.data/data/npcsh/npc_team/send_message.jinx +10 -0
  117. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/serve.jinx +2 -2
  118. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/set.jinx +2 -2
  119. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sh.jinx +1 -1
  120. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sibiji.npc +1 -1
  121. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sleep.jinx +7 -7
  122. npcsh-1.1.17.data/data/npcsh/npc_team/split_pane.jinx +12 -0
  123. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/spool.jinx +4 -4
  124. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +16 -0
  125. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/switch.jinx +3 -3
  126. npcsh-1.1.17.data/data/npcsh/npc_team/switch_npc.jinx +10 -0
  127. npcsh-1.1.17.data/data/npcsh/npc_team/switch_tab.jinx +10 -0
  128. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/switches.jinx +1 -1
  129. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sync.jinx +6 -6
  130. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/teamviz.jinx +2 -2
  131. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/trigger.jinx +2 -2
  132. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/type_text.jinx +1 -1
  133. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/vixynt.jinx +8 -8
  134. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/wait.jinx +1 -1
  135. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +242 -0
  136. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +51 -0
  137. npcsh-1.1.17.data/data/npcsh/npc_team/write_file.jinx +11 -0
  138. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/yap.jinx +5 -5
  139. npcsh-1.1.17.data/data/npcsh/npc_team/zen_mode.jinx +9 -0
  140. {npcsh-1.1.15.dist-info → npcsh-1.1.17.dist-info}/METADATA +10 -7
  141. npcsh-1.1.17.dist-info/RECORD +219 -0
  142. {npcsh-1.1.15.dist-info → npcsh-1.1.17.dist-info}/entry_points.txt +2 -0
  143. npcsh-1.1.15.data/data/npcsh/npc_team/search.jinx +0 -131
  144. npcsh-1.1.15.data/data/npcsh/npc_team/sql.jinx +0 -16
  145. npcsh-1.1.15.data/data/npcsh/npc_team/wander.jinx +0 -152
  146. npcsh-1.1.15.dist-info/RECORD +0 -170
  147. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  148. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/alicanto.png +0 -0
  149. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  150. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/corca.npc +0 -0
  151. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/corca.png +0 -0
  152. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/corca_example.png +0 -0
  153. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/frederic.npc +0 -0
  154. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/frederic4.png +0 -0
  155. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/guac.npc +0 -0
  156. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/guac.png +0 -0
  157. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  158. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  159. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  160. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  161. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  162. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/paste.jinx +0 -0
  163. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/plonk.npc +0 -0
  164. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/plonk.png +0 -0
  165. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  166. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  167. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/python.jinx +0 -0
  168. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/shh.jinx +0 -0
  169. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/sibiji.png +0 -0
  170. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/spool.png +0 -0
  171. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/usage.jinx +0 -0
  172. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  173. {npcsh-1.1.15.data → npcsh-1.1.17.data}/data/npcsh/npc_team/yap.png +0 -0
  174. {npcsh-1.1.15.dist-info → npcsh-1.1.17.dist-info}/WHEEL +0 -0
  175. {npcsh-1.1.15.dist-info → npcsh-1.1.17.dist-info}/licenses/LICENSE +0 -0
  176. {npcsh-1.1.15.dist-info → npcsh-1.1.17.dist-info}/top_level.txt +0 -0
npcsh/_state.py CHANGED
@@ -351,8 +351,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
351
351
  None
352
352
  """
353
353
 
354
- if is_npcsh_initialized():
355
- return
354
+ already_initialized = is_npcsh_initialized()
356
355
 
357
356
  conn = sqlite3.connect(db_path)
358
357
  cursor = conn.cursor()
@@ -368,10 +367,27 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
368
367
  """
369
368
  )
370
369
 
371
- # Package directories
372
- package_dir = os.path.dirname(__file__)
370
+ # Package directories - use helper that handles PyInstaller bundles
371
+ package_dir = get_package_dir()
373
372
  package_npc_team_dir = os.path.join(package_dir, "npc_team")
374
373
 
374
+ # Debug logging for package path resolution
375
+ if os.environ.get("NPCSH_DEBUG", "0") == "1":
376
+ print(f"[DEBUG] Package dir: {package_dir}")
377
+ print(f"[DEBUG] Package npc_team dir: {package_npc_team_dir}")
378
+ print(f"[DEBUG] npc_team exists: {os.path.exists(package_npc_team_dir)}")
379
+ if os.path.exists(package_npc_team_dir):
380
+ print(f"[DEBUG] npc_team contents: {os.listdir(package_npc_team_dir)}")
381
+
382
+ if not os.path.exists(package_npc_team_dir):
383
+ print(f"Warning: Package npc_team directory not found at {package_npc_team_dir}")
384
+ # For bundled executables, try to find it
385
+ if getattr(sys, 'frozen', False):
386
+ print(f"Running as frozen executable, _MEIPASS: {getattr(sys, '_MEIPASS', 'N/A')}")
387
+ if hasattr(sys, '_MEIPASS'):
388
+ print(f"Contents of _MEIPASS: {os.listdir(sys._MEIPASS)}")
389
+ return
390
+
375
391
  user_npc_team_dir = os.path.expanduser("~/.npcsh/npc_team")
376
392
 
377
393
  user_jinxs_dir = os.path.join(user_npc_team_dir, "jinxs")
@@ -491,8 +507,10 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
491
507
  print(f"Copied template {file} to {destination_template_path}")
492
508
  conn.commit()
493
509
  conn.close()
494
- set_npcsh_initialized()
495
- add_npcshrc_to_shell_config()
510
+
511
+ if not already_initialized:
512
+ set_npcsh_initialized()
513
+ add_npcshrc_to_shell_config()
496
514
 
497
515
 
498
516
  def get_shell_config_file() -> str:
@@ -1121,6 +1139,31 @@ def set_npcsh_initialized() -> None:
1121
1139
 
1122
1140
 
1123
1141
 
1142
+ def get_package_dir() -> str:
1143
+ """
1144
+ Get the package directory, handling both normal Python and PyInstaller executables.
1145
+
1146
+ For normal Python: returns os.path.dirname(__file__)
1147
+ For PyInstaller: returns the bundled data directory (sys._MEIPASS/npcsh)
1148
+ """
1149
+ # Check if running as a PyInstaller bundle
1150
+ if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
1151
+ # Running as PyInstaller bundle - look for npcsh folder in _MEIPASS
1152
+ meipass = sys._MEIPASS
1153
+ # The package data should be at _MEIPASS/npcsh (based on PyInstaller config)
1154
+ bundled_path = os.path.join(meipass, 'npcsh')
1155
+ if os.path.exists(bundled_path):
1156
+ return bundled_path
1157
+ # Fallback: check if npc_team is directly in _MEIPASS
1158
+ if os.path.exists(os.path.join(meipass, 'npc_team')):
1159
+ return meipass
1160
+ # Last resort: return meipass and let caller handle
1161
+ return meipass
1162
+ else:
1163
+ # Normal Python execution
1164
+ return os.path.dirname(__file__)
1165
+
1166
+
1124
1167
  def file_has_changed(source_path: str, destination_path: str) -> bool:
1125
1168
  """
1126
1169
  Function Description:
@@ -1134,7 +1177,7 @@ def file_has_changed(source_path: str, destination_path: str) -> bool:
1134
1177
  A boolean indicating whether the files are different
1135
1178
  """
1136
1179
 
1137
-
1180
+
1138
1181
  return not filecmp.cmp(source_path, destination_path, shallow=False)
1139
1182
 
1140
1183
 
@@ -2498,8 +2541,19 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
2498
2541
  if not jinja_env_for_jinx and state.team and isinstance(state.team, Team):
2499
2542
  jinja_env_for_jinx = getattr(state.team, "jinja_env", None)
2500
2543
 
2544
+ jinx_globals = {
2545
+ "state": state,
2546
+ "CommandHistory": CommandHistory,
2547
+ "load_kg_from_db": load_kg_from_db,
2548
+ "execute_rag_command": execute_rag_command,
2549
+ "execute_brainblast_command": execute_brainblast_command,
2550
+ "load_file_contents": load_file_contents,
2551
+ "search_web": search_web,
2552
+ "get_relevant_memories": get_relevant_memories,
2553
+ }
2554
+
2501
2555
  for name, jinx_obj in aggregated_jinxs.items():
2502
- def _make_runner(jinx=jinx_obj, jinja_env=jinja_env_for_jinx, tool_name=name):
2556
+ def _make_runner(jinx=jinx_obj, jinja_env=jinja_env_for_jinx, tool_name=name, extras=jinx_globals):
2503
2557
  def runner(**kwargs):
2504
2558
  input_values = kwargs if isinstance(kwargs, dict) else {}
2505
2559
  try:
@@ -2507,7 +2561,7 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
2507
2561
  input_values=input_values,
2508
2562
  npc=npc_obj,
2509
2563
  messages=state.messages,
2510
- extra_globals={"state": state},
2564
+ extra_globals=extras,
2511
2565
  jinja_env=jinja_env
2512
2566
  )
2513
2567
  return ctx.get("output", ctx)
@@ -2903,14 +2957,17 @@ def process_pipeline_command(
2903
2957
  tool_name = msg.get("name", "tool")
2904
2958
  tool_content = msg.get("content", "")
2905
2959
  if tool_content and tool_content.strip():
2960
+ # Decode escaped newlines if present
2961
+ if isinstance(tool_content, str):
2962
+ tool_content = tool_content.replace('\\n', '\n').replace('\\t', '\t')
2906
2963
  print(colored(f"\n⚡ {tool_name}:", "cyan"))
2907
2964
  lines = tool_content.split('\n')
2908
2965
  if len(lines) > 50:
2909
- print('\n'.join(lines[:25]))
2966
+ render_markdown('\n'.join(lines[:25]))
2910
2967
  print(colored(f"\n... ({len(lines) - 50} lines hidden) ...\n", "white", attrs=["dark"]))
2911
- print('\n'.join(lines[-25:]))
2968
+ render_markdown('\n'.join(lines[-25:]))
2912
2969
  else:
2913
- print(tool_content)
2970
+ render_markdown(tool_content)
2914
2971
 
2915
2972
  # Check if LLM made tool calls - if not, it's done
2916
2973
  tool_calls_made = isinstance(llm_result, dict) and llm_result.get("tool_calls")
@@ -0,0 +1,22 @@
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 .npcsh_agent import NpcshAgent
20
+ from .runner import run_benchmark, BenchmarkRunner
21
+
22
+ __all__ = ["NpcshAgent", "run_benchmark", "BenchmarkRunner"]
@@ -0,0 +1,262 @@
1
+ """
2
+ npcsh Harbor Agent Adapter for Terminal-Bench.
3
+
4
+ This module implements the BaseInstalledAgent interface for running npcsh
5
+ as an agent in Terminal-Bench evaluations.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import shlex
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from harbor.agents.installed.base import BaseInstalledAgent, ExecInput
15
+ from harbor.models.agent.context import AgentContext
16
+
17
+
18
+ class NpcshAgent(BaseInstalledAgent):
19
+ """
20
+ Harbor agent adapter for npcsh.
21
+
22
+ This allows npcsh to be evaluated on Terminal-Bench tasks by:
23
+ 1. Installing npcsh in the benchmark container
24
+ 2. Running npcsh with the task instruction
25
+ 3. Parsing output for token usage and results
26
+
27
+ Usage:
28
+ harbor run -d terminal-bench@2.0 \\
29
+ --agent-import-path npcsh.benchmark:NpcshAgent \\
30
+ -m anthropic/claude-sonnet-4-20250514 -n 4
31
+ """
32
+
33
+ SUPPORTS_ATIF = True # Agent Trajectory Interchange Format
34
+
35
+ def __init__(self, logs_dir: Path = None, model_name: str = None, logger=None, **kwargs):
36
+ super().__init__(logs_dir=logs_dir, model_name=model_name, logger=logger, **kwargs)
37
+
38
+ @staticmethod
39
+ def name() -> str:
40
+ return "npcsh"
41
+
42
+ @property
43
+ def _install_agent_template_path(self) -> Path:
44
+ """Path to the jinja template script for installing npcsh in the container."""
45
+ return Path(__file__).parent / "templates" / "install-npcsh.sh.j2"
46
+
47
+ def create_run_agent_commands(self, instruction: str) -> list:
48
+ """
49
+ Create the commands to run npcsh in the container.
50
+
51
+ Args:
52
+ instruction: The task instruction from Terminal-Bench
53
+
54
+ Returns:
55
+ List of ExecInput commands to execute
56
+ """
57
+ escaped_instruction = shlex.quote(instruction)
58
+ model_name = self.model_name
59
+
60
+ if model_name and "/" in model_name:
61
+ provider, model = model_name.split("/", 1)
62
+ elif model_name:
63
+ provider = os.environ.get("NPCSH_CHAT_PROVIDER", "")
64
+ model = model_name
65
+ else:
66
+ provider = os.environ.get("NPCSH_CHAT_PROVIDER", "")
67
+ model = os.environ.get("NPCSH_CHAT_MODEL", "")
68
+
69
+ # Map provider names to npcsh provider format
70
+ provider_map = {
71
+ "anthropic": "anthropic",
72
+ "openai": "openai",
73
+ "google": "gemini",
74
+ "gemini": "gemini",
75
+ "deepseek": "deepseek",
76
+ "ollama": "ollama",
77
+ "groq": "groq",
78
+ "openrouter": "openrouter",
79
+ }
80
+ npcsh_provider = provider_map.get(provider.lower(), provider)
81
+
82
+ # Build environment variables for API keys
83
+ env_vars = []
84
+ api_key_map = {
85
+ "anthropic": "ANTHROPIC_API_KEY",
86
+ "openai": "OPENAI_API_KEY",
87
+ "gemini": "GOOGLE_API_KEY",
88
+ "google": "GOOGLE_API_KEY",
89
+ "deepseek": "DEEPSEEK_API_KEY",
90
+ "groq": "GROQ_API_KEY",
91
+ "openrouter": "OPENROUTER_API_KEY",
92
+ }
93
+
94
+ for prov, env_key in api_key_map.items():
95
+ if env_key in os.environ:
96
+ env_vars.append(f'{env_key}="{os.environ[env_key]}"')
97
+
98
+ env_prefix = " ".join(env_vars) + " " if env_vars else ""
99
+
100
+ # Output directory for logs
101
+ output_dir = str(self.logs_dir / "npcsh_output")
102
+ output_file = str(self.logs_dir / "npcsh_output" / "output.jsonl")
103
+
104
+ commands = []
105
+
106
+ # Create output directory
107
+ commands.append(ExecInput(
108
+ cmd=f"mkdir -p {shlex.quote(output_dir)}",
109
+ timeout=30
110
+ ))
111
+
112
+ # Run npcsh with the instruction
113
+ # Using the npc CLI which supports single-command execution
114
+ npcsh_cmd = (
115
+ f'{env_prefix}'
116
+ f'NPCSH_CHAT_MODEL="{model}" '
117
+ f'NPCSH_CHAT_PROVIDER="{npcsh_provider}" '
118
+ f'NPCSH_STREAM_OUTPUT=0 '
119
+ f'npc {escaped_instruction} '
120
+ f'2>&1 | tee {shlex.quote(output_file)}'
121
+ )
122
+
123
+ commands.append(ExecInput(
124
+ cmd=npcsh_cmd,
125
+ timeout=600, # 10 minute timeout for complex tasks
126
+ ))
127
+
128
+ return commands
129
+
130
+ def populate_context_post_run(self, context: AgentContext) -> None:
131
+ """
132
+ Populate the context with results of the agent execution.
133
+
134
+ Parses the output file to extract token usage metrics.
135
+
136
+ Args:
137
+ context: The AgentContext to populate with metrics
138
+ """
139
+ output_file = self.logs_dir / "npcsh_output" / "output.jsonl"
140
+
141
+ total_input_tokens = 0
142
+ total_output_tokens = 0
143
+ total_cost_usd = 0.0
144
+
145
+ if output_file.exists():
146
+ try:
147
+ with open(output_file, 'r') as f:
148
+ content = f.read()
149
+
150
+ # Try to parse as JSONL first
151
+ for line in content.strip().split('\n'):
152
+ if not line.strip():
153
+ continue
154
+ try:
155
+ event = json.loads(line)
156
+ # Extract token usage from events if present
157
+ if isinstance(event, dict):
158
+ usage = event.get('usage', {})
159
+ total_input_tokens += usage.get('input_tokens', 0)
160
+ total_output_tokens += usage.get('output_tokens', 0)
161
+ total_cost_usd += usage.get('cost_usd', 0.0)
162
+ except json.JSONDecodeError:
163
+ # Not JSON, just regular output
164
+ pass
165
+
166
+ except Exception as e:
167
+ self.logger.warning(f"Failed to parse npcsh output: {e}")
168
+
169
+ # Set context metrics
170
+ if hasattr(context, 'input_tokens'):
171
+ context.input_tokens = total_input_tokens
172
+ if hasattr(context, 'output_tokens'):
173
+ context.output_tokens = total_output_tokens
174
+ if hasattr(context, 'cost_usd'):
175
+ context.cost_usd = total_cost_usd
176
+
177
+
178
+ class NpcshAgentWithNpc(NpcshAgent):
179
+ """
180
+ Variant that uses a specific NPC for task execution.
181
+
182
+ This allows benchmarking specific NPCs like sibiji (orchestrator),
183
+ corca (coding), or custom NPCs.
184
+
185
+ Usage:
186
+ harbor run -d terminal-bench@2.0 \\
187
+ --agent-import-path "npcsh.benchmark:NpcshAgentWithNpc" \\
188
+ -m anthropic/claude-sonnet-4-20250514 -n 4
189
+ """
190
+
191
+ def __init__(self, *args, npc_name: str = "sibiji", **kwargs):
192
+ super().__init__(*args, **kwargs)
193
+ self.npc_name = npc_name
194
+
195
+ @staticmethod
196
+ def name() -> str:
197
+ return "npcsh-npc"
198
+
199
+ def create_run_agent_commands(self, instruction: str) -> list:
200
+ """Create commands using a specific NPC."""
201
+ escaped_instruction = shlex.quote(instruction)
202
+ model_name = self.model_name
203
+
204
+ if model_name and "/" in model_name:
205
+ provider, model = model_name.split("/", 1)
206
+ elif model_name:
207
+ provider = os.environ.get("NPCSH_CHAT_PROVIDER", "")
208
+ model = model_name
209
+ else:
210
+ provider = os.environ.get("NPCSH_CHAT_PROVIDER", "")
211
+ model = os.environ.get("NPCSH_CHAT_MODEL", "")
212
+
213
+ provider_map = {
214
+ "anthropic": "anthropic",
215
+ "openai": "openai",
216
+ "google": "gemini",
217
+ "gemini": "gemini",
218
+ "deepseek": "deepseek",
219
+ "ollama": "ollama",
220
+ }
221
+ npcsh_provider = provider_map.get(provider.lower(), provider)
222
+
223
+ env_vars = []
224
+ api_key_map = {
225
+ "anthropic": "ANTHROPIC_API_KEY",
226
+ "openai": "OPENAI_API_KEY",
227
+ "gemini": "GOOGLE_API_KEY",
228
+ "deepseek": "DEEPSEEK_API_KEY",
229
+ }
230
+
231
+ for prov, env_key in api_key_map.items():
232
+ if env_key in os.environ:
233
+ env_vars.append(f'{env_key}="{os.environ[env_key]}"')
234
+
235
+ env_prefix = " ".join(env_vars) + " " if env_vars else ""
236
+
237
+ output_dir = str(self.logs_dir / "npcsh_output")
238
+ output_file = str(self.logs_dir / "npcsh_output" / "output.jsonl")
239
+
240
+ commands = []
241
+
242
+ commands.append(ExecInput(
243
+ cmd=f"mkdir -p {shlex.quote(output_dir)}",
244
+ timeout=30
245
+ ))
246
+
247
+ # Use specific NPC with --npc flag
248
+ npcsh_cmd = (
249
+ f'{env_prefix}'
250
+ f'NPCSH_CHAT_MODEL="{model}" '
251
+ f'NPCSH_CHAT_PROVIDER="{npcsh_provider}" '
252
+ f'NPCSH_STREAM_OUTPUT=0 '
253
+ f'npc --npc {self.npc_name} {escaped_instruction} '
254
+ f'2>&1 | tee {shlex.quote(output_file)}'
255
+ )
256
+
257
+ commands.append(ExecInput(
258
+ cmd=npcsh_cmd,
259
+ timeout=600,
260
+ ))
261
+
262
+ return commands