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
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,
@@ -113,6 +125,7 @@ from .config import (
113
125
  NPCSH_API_URL,
114
126
  NPCSH_SEARCH_PROVIDER,
115
127
  NPCSH_BUILD_KG,
128
+ NPCSH_EDIT_APPROVAL,
116
129
  setup_npcsh_config,
117
130
  is_npcsh_initialized,
118
131
  set_npcsh_initialized,
@@ -156,6 +169,9 @@ class ShellState:
156
169
  video_gen_provider: str = NPCSH_VIDEO_GEN_PROVIDER
157
170
  current_mode: str = NPCSH_DEFAULT_MODE
158
171
  build_kg: bool = NPCSH_BUILD_KG
172
+ kg_link_facts: bool = False # Link facts to concepts (requires LLM calls)
173
+ kg_link_concepts: bool = False # Link concepts to concepts (requires LLM calls)
174
+ kg_link_facts_facts: bool = False # Link facts to facts (requires LLM calls)
159
175
  api_key: Optional[str] = None
160
176
  api_url: Optional[str] = NPCSH_API_URL
161
177
  current_path: str = field(default_factory=os.getcwd)
@@ -170,6 +186,10 @@ class ShellState:
170
186
  session_start_time: float = field(default_factory=lambda: __import__('time').time())
171
187
  # Logging level: "silent", "normal", "verbose"
172
188
  log_level: str = "normal"
189
+ # Edit approval mode: "off", "interactive", "auto"
190
+ edit_approval: str = NPCSH_EDIT_APPROVAL
191
+ # Pending file edits for approval
192
+ pending_edits: Dict[str, Dict[str, str]] = field(default_factory=dict)
173
193
 
174
194
  def get_model_for_command(self, model_type: str = "chat"):
175
195
  if model_type == "chat":
@@ -256,6 +276,8 @@ CONFIG_KEY_MAP = {
256
276
  "stream": "NPCSH_STREAM_OUTPUT",
257
277
  "apiurl": "NPCSH_API_URL",
258
278
  "buildkg": "NPCSH_BUILD_KG",
279
+ "editapproval": "NPCSH_EDIT_APPROVAL",
280
+ "approval": "NPCSH_EDIT_APPROVAL",
259
281
  }
260
282
 
261
283
 
@@ -300,9 +322,37 @@ def set_npcsh_config_value(key: str, value: str):
300
322
  "NPCSH_BUILD_KG": "build_kg",
301
323
  "NPCSH_API_URL": "api_url",
302
324
  "NPCSH_STREAM_OUTPUT": "stream_output",
325
+ "NPCSH_EDIT_APPROVAL": "edit_approval",
303
326
  }
304
327
  if env_key in field_map:
305
328
  setattr(ShellState, field_map[env_key], parsed_val)
329
+
330
+ # Persist to ~/.npcshrc
331
+ npcshrc_path = os.path.expanduser("~/.npcshrc")
332
+ try:
333
+ existing_lines = []
334
+ if os.path.exists(npcshrc_path):
335
+ with open(npcshrc_path, 'r') as f:
336
+ existing_lines = f.readlines()
337
+
338
+ # Update or add the export line
339
+ export_line = f"export {env_key}=\"{value}\"\n"
340
+ found = False
341
+ for i, line in enumerate(existing_lines):
342
+ if line.strip().startswith(f"export {env_key}="):
343
+ existing_lines[i] = export_line
344
+ found = True
345
+ break
346
+
347
+ if not found:
348
+ existing_lines.append(export_line)
349
+
350
+ with open(npcshrc_path, 'w') as f:
351
+ f.writelines(existing_lines)
352
+ except Exception as e:
353
+ print(f"Warning: Could not persist config to {npcshrc_path}: {e}")
354
+
355
+
306
356
  def get_npc_path(npc_name: str, db_path: str) -> str:
307
357
  project_npc_team_dir = os.path.abspath("./npc_team")
308
358
  project_npc_path = os.path.join(project_npc_team_dir, f"{npc_name}.npc")
@@ -317,7 +367,7 @@ def get_npc_path(npc_name: str, db_path: str) -> str:
317
367
  if result:
318
368
  return result[0]
319
369
 
320
- except Exception as e:
370
+ except Exception:
321
371
  try:
322
372
  with sqlite3.connect(db_path) as conn:
323
373
  cursor = conn.cursor()
@@ -424,10 +474,10 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
424
474
  old_package_jinxs = set()
425
475
  if os.path.exists(manifest_path):
426
476
  try:
427
- import json
477
+
428
478
  with open(manifest_path, 'r') as f:
429
479
  old_package_jinxs = set(json.load(f).get('jinxs', []))
430
- except:
480
+ except Exception:
431
481
  pass
432
482
 
433
483
  # Track current package jinxs
@@ -480,7 +530,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
480
530
 
481
531
  # Save updated manifest
482
532
  try:
483
- import json
533
+
484
534
  with open(manifest_path, 'w') as f:
485
535
  json.dump({'jinxs': list(current_package_jinxs), 'updated': str(__import__('datetime').datetime.now())}, f, indent=2)
486
536
  except Exception as e:
@@ -561,9 +611,6 @@ def get_relevant_memories(
561
611
  max_memories: int = 10,
562
612
  state: Optional[ShellState] = None
563
613
  ) -> List[Dict]:
564
-
565
- engine = command_history.engine
566
-
567
614
  all_memories = command_history.get_memories_for_scope(
568
615
  npc=npc_name,
569
616
  team=team_name,
@@ -588,7 +635,7 @@ def get_relevant_memories(
588
635
 
589
636
  if state and state.embedding_model and state.embedding_provider:
590
637
  try:
591
- from npcpy.gen.embeddings import get_embeddings
638
+
592
639
 
593
640
  search_text = query if query else "recent context"
594
641
  query_embedding = get_embeddings(
@@ -606,7 +653,7 @@ def get_relevant_memories(
606
653
  state.embedding_provider
607
654
  )
608
655
 
609
- import numpy as np
656
+
610
657
  similarities = []
611
658
  for mem_emb in memory_embeddings:
612
659
  similarity = np.dot(query_embedding, mem_emb) / (
@@ -816,7 +863,6 @@ BASH_COMMANDS = [
816
863
  "command",
817
864
  "compgen",
818
865
  "complete",
819
- "continue",
820
866
  "declare",
821
867
  "dirs",
822
868
  "disown",
@@ -1283,7 +1329,7 @@ def get_setting_windows(key, default=None):
1283
1329
 
1284
1330
 
1285
1331
  def setup_readline() -> str:
1286
- import readline
1332
+
1287
1333
  if readline is None:
1288
1334
  return None
1289
1335
  try:
@@ -1429,7 +1475,7 @@ def make_completer(shell_state: ShellState, router: Any):
1429
1475
  else:
1430
1476
  return None # readline expects None when no more completions
1431
1477
 
1432
- except Exception as e:
1478
+ except Exception:
1433
1479
  # Using completion_logger for internal debugging, not printing to stdout for user.
1434
1480
  # completion_logger.error(f"Exception in completion: {e}", exc_info=True)
1435
1481
  return None
@@ -1589,7 +1635,7 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1589
1635
  try:
1590
1636
  import termios
1591
1637
  import tty
1592
- import readline
1638
+
1593
1639
  except ImportError:
1594
1640
  return input(prompt)
1595
1641
 
@@ -1625,7 +1671,7 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1625
1671
  try:
1626
1672
  import shutil
1627
1673
  term_width = shutil.get_terminal_size().columns
1628
- except:
1674
+ except json.JSONDecodeError:
1629
1675
  term_width = 80
1630
1676
 
1631
1677
  def draw():
@@ -1638,7 +1684,6 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1638
1684
  sys.stdout.write('\r')
1639
1685
  # Move up for each wrapped line we're on
1640
1686
  cursor_total = prompt_visible_len + pos
1641
- cursor_line = cursor_total // term_width
1642
1687
  # Go up to the first line of input
1643
1688
  for _ in range(num_lines - 1):
1644
1689
  sys.stdout.write('\033[A')
@@ -1721,7 +1766,7 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1721
1766
  # Check if this looks like binary/image data
1722
1767
  # Image signatures: PNG (\x89PNG), JPEG (\xff\xd8\xff), GIF (GIF8), BMP (BM)
1723
1768
  # Also check for high ratio of non-printable chars
1724
- is_binary = False
1769
+
1725
1770
  if len(paste_buffer) > 4:
1726
1771
  # Check for common image magic bytes
1727
1772
  if paste_buffer[:4] == '\x89PNG' or paste_buffer[:8] == '\x89PNG\r\n\x1a\n':
@@ -1742,8 +1787,8 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1742
1787
 
1743
1788
  if is_binary:
1744
1789
  # Save image data to temp file
1745
- import tempfile
1746
- import os
1790
+
1791
+
1747
1792
  try:
1748
1793
  # Determine extension from magic bytes
1749
1794
  ext = '.bin'
@@ -1766,14 +1811,14 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1766
1811
  with os.fdopen(fd, 'wb') as f:
1767
1812
  if paste_buffer.startswith('data:image/'):
1768
1813
  # Decode base64 data URL
1769
- import base64
1814
+
1770
1815
  _, data = paste_buffer.split(',', 1)
1771
1816
  f.write(base64.b64decode(data))
1772
1817
  else:
1773
1818
  f.write(paste_buffer.encode('latin-1'))
1774
1819
  pasted_content = temp_path # Store path to image
1775
1820
  placeholder = f"[pasted image: {temp_path}]"
1776
- except:
1821
+ except Exception:
1777
1822
  pasted_content = None
1778
1823
  placeholder = "[pasted image: failed to save]"
1779
1824
  else:
@@ -1971,7 +2016,7 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
1971
2016
  sys.stdout.flush()
1972
2017
  else:
1973
2018
  pass # No tool call to show
1974
- except:
2019
+ except Exception:
1975
2020
  pass
1976
2021
 
1977
2022
  elif c and ord(c) >= 32: # Printable
@@ -2000,7 +2045,7 @@ def _get_slash_hints(state, router, prefix='/') -> str:
2000
2045
  try:
2001
2046
  import shutil
2002
2047
  term_width = shutil.get_terminal_size().columns
2003
- except:
2048
+ except Exception:
2004
2049
  term_width = 80
2005
2050
 
2006
2051
  # Build hint string that fits in terminal
@@ -2149,44 +2194,20 @@ def wrap_text(text: str, width: int = 80) -> str:
2149
2194
 
2150
2195
 
2151
2196
 
2152
- def setup_readline() -> str:
2153
- """Setup readline with history and completion"""
2154
- try:
2155
- readline.read_history_file(READLINE_HISTORY_FILE)
2156
- readline.set_history_length(1000)
2157
-
2158
-
2159
- readline.parse_and_bind("tab: complete")
2160
-
2161
- readline.parse_and_bind("set enable-bracketed-paste on")
2162
- readline.parse_and_bind(r'"\C-r": reverse-search-history')
2163
- readline.parse_and_bind(r'"\C-e": end-of-line')
2164
- readline.parse_and_bind(r'"\C-a": beginning-of-line')
2165
-
2166
- return READLINE_HISTORY_FILE
2167
-
2168
- except FileNotFoundError:
2169
- pass
2170
- except OSError as e:
2171
- print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
2172
2197
 
2173
-
2174
- def save_readline_history():
2175
- try:
2176
- readline.write_history_file(READLINE_HISTORY_FILE)
2177
- except OSError as e:
2178
- print(f"Warning: Could not write readline history file {READLINE_HISTORY_FILE}: {e}")
2179
2198
 
2180
2199
  def store_command_embeddings(command: str, output: Any, state: ShellState):
2181
2200
  if not chroma_client or not state.embedding_model or not state.embedding_provider:
2182
- if not chroma_client: print("Warning: ChromaDB client not available for embeddings.", file=sys.stderr)
2201
+ if not chroma_client:
2202
+ print("Warning: ChromaDB client not available for embeddings.", file=sys.stderr)
2183
2203
  return
2184
2204
  if not command and not output:
2185
2205
  return
2186
2206
 
2187
2207
  try:
2188
2208
  output_str = str(output) if output else ""
2189
- if not command and not output_str: return
2209
+ if not command and not output_str:
2210
+ return
2190
2211
 
2191
2212
  texts_to_embed = [command, output_str]
2192
2213
 
@@ -2358,10 +2379,7 @@ def _ollama_supports_tools(model: str) -> Optional[bool]:
2358
2379
  Best-effort check for tool-call support on an Ollama model by inspecting its template/metadata.
2359
2380
  Mirrors the lightweight check used in the Flask serve path.
2360
2381
  """
2361
- try:
2362
- import ollama # Local import to avoid hard dependency when Ollama isn't installed
2363
- except Exception:
2364
- return None
2382
+
2365
2383
 
2366
2384
  try:
2367
2385
  details = ollama.show(model)
@@ -2468,7 +2486,7 @@ def wrap_tool_with_display(tool_name: str, tool_func: Callable, state: ShellStat
2468
2486
  print(colored(f" ⚡ {tool_name}", "cyan") + colored(f" {args_display}", "white", attrs=["dark"]), end="", flush=True)
2469
2487
  else:
2470
2488
  print(colored(f" ⚡ {tool_name}", "cyan"), end="", flush=True)
2471
- except:
2489
+ except Exception:
2472
2490
  pass
2473
2491
 
2474
2492
  # Execute tool
@@ -2484,14 +2502,14 @@ def wrap_tool_with_display(tool_name: str, tool_func: Callable, state: ShellStat
2484
2502
  result_preview = result_preview[:200] + "..."
2485
2503
  if result_preview and result_preview not in ('None', '', '{}', '[]'):
2486
2504
  print(colored(f" → {result_preview}", "white", attrs=["dark"]), flush=True)
2487
- except:
2505
+ except Exception:
2488
2506
  pass
2489
2507
  return result
2490
2508
  except Exception as e:
2491
2509
  if log_level != "silent":
2492
2510
  try:
2493
2511
  print(colored(f" ✗ {str(e)[:100]}", "red"), flush=True)
2494
- except:
2512
+ except Exception as e:
2495
2513
  pass
2496
2514
  raise
2497
2515
  return wrapped
@@ -2642,10 +2660,10 @@ def should_skip_kg_processing(user_input: str, assistant_output: str) -> bool:
2642
2660
 
2643
2661
  return False
2644
2662
 
2645
- def execute_slash_command(command: str,
2646
- stdin_input: Optional[str],
2647
- state: ShellState,
2648
- stream: bool,
2663
+ def execute_slash_command(command: str,
2664
+ stdin_input: Optional[str],
2665
+ state: ShellState,
2666
+ stream: bool,
2649
2667
  router) -> Tuple[ShellState, Any]:
2650
2668
  """Executes slash commands using the router."""
2651
2669
  try:
@@ -2653,7 +2671,13 @@ def execute_slash_command(command: str,
2653
2671
  except ValueError:
2654
2672
  all_command_parts = command.split()
2655
2673
  command_name = all_command_parts[0].lstrip('/')
2674
+
2675
+ # --- QUIT/EXIT HANDLING ---
2676
+ if command_name in ['quit', 'exit', 'q']:
2656
2677
 
2678
+ print("Goodbye!")
2679
+ sys.exit(0)
2680
+
2657
2681
  # --- NPC SWITCHING LOGIC ---
2658
2682
  if command_name in ['n', 'npc']:
2659
2683
  npc_to_switch_to = all_command_parts[1] if len(all_command_parts) > 1 else None
@@ -2914,9 +2938,6 @@ def process_pipeline_command(
2914
2938
  "tools": tools_for_llm,
2915
2939
  "tool_map": tool_exec_map,
2916
2940
  }
2917
- # Only add tool_choice for providers that support it (not gemini)
2918
- is_gemini = (exec_provider and "gemini" in exec_provider.lower()) or \
2919
- (exec_model and "gemini" in exec_model.lower())
2920
2941
  llm_kwargs["tool_choice"] = 'auto'
2921
2942
 
2922
2943
  # Agent loop: keep calling LLM until it stops making tool calls
@@ -3105,7 +3126,7 @@ def _delegate_to_npc(state: ShellState, npc_name: str, command: str, delegation_
3105
3126
  MAX_DELEGATION_DEPTH = 1 # Only allow one level of delegation
3106
3127
 
3107
3128
  if delegation_depth > MAX_DELEGATION_DEPTH:
3108
- return state, {'output': f"⚠ Maximum delegation depth reached."}
3129
+ return state, {'output': "⚠ Maximum delegation depth reached."}
3109
3130
 
3110
3131
  if not state.team or not hasattr(state.team, 'npcs') or npc_name not in state.team.npcs:
3111
3132
  return state, {'output': f"⚠ NPC '{npc_name}' not found in team"}
@@ -3311,7 +3332,7 @@ def execute_command(
3311
3332
  )
3312
3333
  )
3313
3334
  stdin_for_next = full_stream_output
3314
- except:
3335
+ except Exception:
3315
3336
  if output is not None:
3316
3337
  try:
3317
3338
  stdin_for_next = str(output)
@@ -3413,7 +3434,7 @@ def execute_command(
3413
3434
  def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3414
3435
  setup_npcsh_config()
3415
3436
 
3416
- db_path = os.getenv("NPCSH_DB_PATH", HISTORY_DB_DEFAULT_PATH)
3437
+ db_path = NPCSH_DB_PATH
3417
3438
  db_path = os.path.expanduser(db_path)
3418
3439
  os.makedirs(os.path.dirname(db_path), exist_ok=True)
3419
3440
  command_history = CommandHistory(db_path)
@@ -3424,11 +3445,11 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3424
3445
  print("NPCSH initialization complete. Restart or source ~/.npcshrc.")
3425
3446
 
3426
3447
  try:
3427
- history_file = setup_readline()
3448
+ setup_readline()
3428
3449
  atexit.register(save_readline_history)
3429
3450
  atexit.register(command_history.close)
3430
- except:
3431
- pass
3451
+ except OSError as e:
3452
+ print(f"Warning: Failed to setup readline history: {e}", file=sys.stderr)
3432
3453
 
3433
3454
  project_team_path = os.path.abspath(PROJECT_NPC_TEAM_PATH)
3434
3455
  global_team_path = os.path.expanduser(DEFAULT_NPC_TEAM_PATH)
@@ -3437,7 +3458,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3437
3458
  default_forenpc_name = None
3438
3459
  global_team_path = os.path.expanduser(DEFAULT_NPC_TEAM_PATH)
3439
3460
  if not os.path.exists(global_team_path):
3440
- print(f"Global NPC team directory doesn't exist. Initializing...")
3461
+ print("Global NPC team directory doesn't exist. Initializing...")
3441
3462
  initialize_base_npcs_if_needed(db_path)
3442
3463
  if os.path.exists(project_team_path):
3443
3464
  team_dir = project_team_path
@@ -3657,6 +3678,10 @@ def process_result(
3657
3678
  final_output_str = None
3658
3679
 
3659
3680
  # FIX: Handle dict output properly
3681
+ msg_input_tokens = None
3682
+ msg_output_tokens = None
3683
+ msg_cost = None
3684
+
3660
3685
  if isinstance(output, dict):
3661
3686
  # Use None-safe check to not skip empty strings
3662
3687
  output_content = output.get('output') if 'output' in output else output.get('response')
@@ -3666,15 +3691,18 @@ def process_result(
3666
3691
  # Accumulate token usage if available
3667
3692
  if 'usage' in output:
3668
3693
  usage = output['usage']
3669
- result_state.session_input_tokens += usage.get('input_tokens', 0)
3670
- result_state.session_output_tokens += usage.get('output_tokens', 0)
3694
+ msg_input_tokens = usage.get('input_tokens', 0)
3695
+ msg_output_tokens = usage.get('output_tokens', 0)
3696
+ result_state.session_input_tokens += msg_input_tokens
3697
+ result_state.session_output_tokens += msg_output_tokens
3671
3698
  # Calculate cost
3672
3699
  from npcpy.gen.response import calculate_cost
3673
- result_state.session_cost_usd += calculate_cost(
3700
+ msg_cost = calculate_cost(
3674
3701
  model_for_stream,
3675
- usage.get('input_tokens', 0),
3676
- usage.get('output_tokens', 0)
3702
+ msg_input_tokens,
3703
+ msg_output_tokens
3677
3704
  )
3705
+ result_state.session_cost_usd += msg_cost
3678
3706
 
3679
3707
  # If output_content is still a dict, convert to string
3680
3708
  if isinstance(output_content, dict):
@@ -3736,6 +3764,9 @@ def process_result(
3736
3764
  provider=active_npc.provider,
3737
3765
  npc=npc_name,
3738
3766
  team=team_name,
3767
+ input_tokens=msg_input_tokens,
3768
+ output_tokens=msg_output_tokens,
3769
+ cost=msg_cost,
3739
3770
  )
3740
3771
 
3741
3772
  result_state.turn_count += 1
@@ -3844,15 +3875,15 @@ def process_result(
3844
3875
  result_state.current_path
3845
3876
  )
3846
3877
  evolved_npc_kg, _ = kg_evolve_incremental(
3847
- existing_kg=npc_kg,
3878
+ existing_kg=npc_kg,
3848
3879
  new_facts=approved_facts,
3849
- model=active_npc.model,
3850
- provider=active_npc.provider,
3880
+ model=active_npc.model,
3881
+ provider=active_npc.provider,
3851
3882
  npc=active_npc,
3852
3883
  get_concepts=True,
3853
- link_concepts_facts=False,
3854
- link_concepts_concepts=False,
3855
- link_facts_facts=False,
3884
+ link_concepts_facts=result_state.kg_link_facts,
3885
+ link_concepts_concepts=result_state.kg_link_concepts,
3886
+ link_facts_facts=result_state.kg_link_facts_facts,
3856
3887
  )
3857
3888
  save_kg_to_db(
3858
3889
  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.")
@@ -16,7 +16,13 @@ Usage:
16
16
  run_benchmark(model="claude-sonnet-4-20250514", provider="anthropic")
17
17
  """
18
18
 
19
- from .npcsh_agent import NpcshAgent
20
19
  from .runner import run_benchmark, BenchmarkRunner
21
20
 
22
- __all__ = ["NpcshAgent", "run_benchmark", "BenchmarkRunner"]
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