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/diff_viewer.py ADDED
@@ -0,0 +1,452 @@
1
+ """
2
+ Git-diff approval TUI for npcsh.
3
+ Provides interactive diff viewing with approve/reject functionality.
4
+ """
5
+ import os
6
+ import sys
7
+ import difflib
8
+ from dataclasses import dataclass, field
9
+ from typing import List, Dict, Optional, Tuple
10
+ from enum import Enum
11
+
12
+ # Platform-specific imports
13
+ try:
14
+ import tty
15
+ import termios
16
+ import select
17
+ HAS_TTY = True
18
+ except ImportError:
19
+ HAS_TTY = False
20
+
21
+
22
+ class HunkDecision(Enum):
23
+ PENDING = "pending"
24
+ APPROVED = "approved"
25
+ REJECTED = "rejected"
26
+
27
+
28
+ @dataclass
29
+ class DiffHunk:
30
+ """Represents a single diff hunk"""
31
+ start_original: int
32
+ count_original: int
33
+ start_modified: int
34
+ count_modified: int
35
+ lines: List[str]
36
+ header: str
37
+
38
+
39
+ @dataclass
40
+ class DiffViewerState:
41
+ """State for the diff viewer TUI"""
42
+ file_path: str
43
+ original: str
44
+ modified: str
45
+ hunks: List[DiffHunk] = field(default_factory=list)
46
+ decisions: Dict[int, HunkDecision] = field(default_factory=dict)
47
+ selected_hunk: int = 0
48
+ scroll_offset: int = 0
49
+ mode: str = "normal" # normal, help
50
+
51
+
52
+ def compute_diff_hunks(original: str, modified: str) -> List[DiffHunk]:
53
+ """Compute diff hunks between original and modified content."""
54
+ original_lines = original.splitlines(keepends=True)
55
+ modified_lines = modified.splitlines(keepends=True)
56
+
57
+ diff = list(difflib.unified_diff(
58
+ original_lines,
59
+ modified_lines,
60
+ lineterm=''
61
+ ))
62
+
63
+ hunks = []
64
+ current_hunk_lines = []
65
+ current_header = ""
66
+ start_orig = 0
67
+ count_orig = 0
68
+ start_mod = 0
69
+ count_mod = 0
70
+
71
+ for line in diff[2:]: # Skip the --- and +++ headers
72
+ if line.startswith('@@'):
73
+ # Save previous hunk if exists
74
+ if current_hunk_lines:
75
+ hunks.append(DiffHunk(
76
+ start_original=start_orig,
77
+ count_original=count_orig,
78
+ start_modified=start_mod,
79
+ count_modified=count_mod,
80
+ lines=current_hunk_lines,
81
+ header=current_header
82
+ ))
83
+
84
+ # Parse new hunk header
85
+ current_header = line.strip()
86
+ current_hunk_lines = []
87
+
88
+ # Parse @@ -start,count +start,count @@
89
+ try:
90
+ parts = line.split('@@')[1].strip().split()
91
+ orig_part = parts[0] # -start,count
92
+ mod_part = parts[1] # +start,count
93
+
94
+ if ',' in orig_part:
95
+ start_orig, count_orig = map(int, orig_part[1:].split(','))
96
+ else:
97
+ start_orig = int(orig_part[1:])
98
+ count_orig = 1
99
+
100
+ if ',' in mod_part:
101
+ start_mod, count_mod = map(int, mod_part[1:].split(','))
102
+ else:
103
+ start_mod = int(mod_part[1:])
104
+ count_mod = 1
105
+ except (IndexError, ValueError):
106
+ start_orig = count_orig = start_mod = count_mod = 0
107
+ else:
108
+ current_hunk_lines.append(line)
109
+
110
+ # Save last hunk
111
+ if current_hunk_lines:
112
+ hunks.append(DiffHunk(
113
+ start_original=start_orig,
114
+ count_original=count_orig,
115
+ start_modified=start_mod,
116
+ count_modified=count_mod,
117
+ lines=current_hunk_lines,
118
+ header=current_header
119
+ ))
120
+
121
+ return hunks
122
+
123
+
124
+ def get_terminal_size() -> Tuple[int, int]:
125
+ """Get terminal size (width, height)."""
126
+ try:
127
+ size = os.get_terminal_size()
128
+ return size.columns, size.lines
129
+ except:
130
+ return 80, 24
131
+
132
+
133
+ class DiffViewer:
134
+ """Interactive diff viewer with approve/reject functionality."""
135
+
136
+ def __init__(self, file_path: str, original: str, modified: str):
137
+ self.state = DiffViewerState(
138
+ file_path=file_path,
139
+ original=original,
140
+ modified=modified
141
+ )
142
+ self.state.hunks = compute_diff_hunks(original, modified)
143
+
144
+ # Initialize all hunks as pending
145
+ for i in range(len(self.state.hunks)):
146
+ self.state.decisions[i] = HunkDecision.PENDING
147
+
148
+ def render_screen(self):
149
+ """Render the diff viewer screen."""
150
+ width, height = get_terminal_size()
151
+ out = []
152
+
153
+ # Clear screen and move to top
154
+ out.append("\033[2J\033[H")
155
+
156
+ # Header
157
+ header = f" File Edit: {self.state.file_path} "
158
+ if len(header) > width - 4:
159
+ header = f" ...{self.state.file_path[-width+15:]} "
160
+ out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
161
+ out.append(f"\033[1;2H\033[44;37;1m{header}\033[0m")
162
+
163
+ # Stats line
164
+ approved = sum(1 for d in self.state.decisions.values() if d == HunkDecision.APPROVED)
165
+ rejected = sum(1 for d in self.state.decisions.values() if d == HunkDecision.REJECTED)
166
+ pending = sum(1 for d in self.state.decisions.values() if d == HunkDecision.PENDING)
167
+ stats = f"Hunks: {len(self.state.hunks)} | Approved: {approved} | Rejected: {rejected} | Pending: {pending}"
168
+ out.append(f"\033[2;1H\033[90m{stats.center(width)}\033[0m")
169
+
170
+ # Diff content area
171
+ content_start = 4
172
+ content_height = height - 6
173
+
174
+ if not self.state.hunks:
175
+ out.append(f"\033[{content_start};2H\033[33mNo differences found.\033[0m")
176
+ else:
177
+ # Render current hunk
178
+ hunk = self.state.hunks[self.state.selected_hunk]
179
+ decision = self.state.decisions[self.state.selected_hunk]
180
+
181
+ # Hunk header with decision indicator
182
+ decision_indicator = {
183
+ HunkDecision.PENDING: "\033[33m[?]\033[0m",
184
+ HunkDecision.APPROVED: "\033[32m[+]\033[0m",
185
+ HunkDecision.REJECTED: "\033[31m[-]\033[0m"
186
+ }[decision]
187
+
188
+ hunk_header = f"{decision_indicator} Hunk {self.state.selected_hunk + 1}/{len(self.state.hunks)}: {hunk.header}"
189
+ out.append(f"\033[3;1H\033[90m{'-' * width}\033[0m")
190
+ out.append(f"\033[3;2H{hunk_header[:width-4]}")
191
+
192
+ # Render diff lines
193
+ visible_lines = hunk.lines[self.state.scroll_offset:self.state.scroll_offset + content_height]
194
+
195
+ for i, line in enumerate(visible_lines):
196
+ row = content_start + i
197
+ if row >= height - 2:
198
+ break
199
+
200
+ # Color based on line type
201
+ if line.startswith('+'):
202
+ color = "\033[32m" # Green for additions
203
+ elif line.startswith('-'):
204
+ color = "\033[31m" # Red for deletions
205
+ else:
206
+ color = "\033[0m" # Default for context
207
+
208
+ # Truncate long lines
209
+ display_line = line.rstrip()[:width-2]
210
+ out.append(f"\033[{row};1H{color}{display_line}\033[0m")
211
+
212
+ # Scroll indicator
213
+ if len(hunk.lines) > content_height:
214
+ scroll_pct = (self.state.scroll_offset / (len(hunk.lines) - content_height)) * 100
215
+ scroll_info = f"[{int(scroll_pct)}%]"
216
+ out.append(f"\033[{content_start};{width-len(scroll_info)-1}H\033[90m{scroll_info}\033[0m")
217
+
218
+ # Footer with keybindings
219
+ footer_y = height - 1
220
+ out.append(f"\033[{footer_y};1H\033[90m{'-' * width}\033[0m")
221
+
222
+ keys = "[a] Approve [r] Reject [A] Approve All [R] Reject All [j/k] Hunks [q] Done [?] Help"
223
+ out.append(f"\033[{height};1H\033[90m{keys[:width]}\033[0m")
224
+
225
+ sys.stdout.write(''.join(out))
226
+ sys.stdout.flush()
227
+
228
+ def handle_input(self, c: str) -> bool:
229
+ """Handle input character. Returns False to exit."""
230
+ if c == 'q':
231
+ return False
232
+
233
+ elif c == 'a': # Approve current hunk
234
+ self.state.decisions[self.state.selected_hunk] = HunkDecision.APPROVED
235
+ if self.state.selected_hunk < len(self.state.hunks) - 1:
236
+ self.state.selected_hunk += 1
237
+ self.state.scroll_offset = 0
238
+
239
+ elif c == 'r': # Reject current hunk
240
+ self.state.decisions[self.state.selected_hunk] = HunkDecision.REJECTED
241
+ if self.state.selected_hunk < len(self.state.hunks) - 1:
242
+ self.state.selected_hunk += 1
243
+ self.state.scroll_offset = 0
244
+
245
+ elif c == 'A': # Approve all
246
+ for i in range(len(self.state.hunks)):
247
+ self.state.decisions[i] = HunkDecision.APPROVED
248
+
249
+ elif c == 'R': # Reject all
250
+ for i in range(len(self.state.hunks)):
251
+ self.state.decisions[i] = HunkDecision.REJECTED
252
+
253
+ elif c == 'j' or c == '\x1b': # Down/next hunk (or escape sequence)
254
+ if c == '\x1b':
255
+ # Handle escape sequences
256
+ if HAS_TTY and select.select([sys.stdin], [], [], 0.05)[0]:
257
+ c2 = sys.stdin.read(1)
258
+ if c2 == '[':
259
+ c3 = sys.stdin.read(1)
260
+ if c3 == 'B': # Down arrow
261
+ if self.state.selected_hunk < len(self.state.hunks) - 1:
262
+ self.state.selected_hunk += 1
263
+ self.state.scroll_offset = 0
264
+ elif c3 == 'A': # Up arrow
265
+ if self.state.selected_hunk > 0:
266
+ self.state.selected_hunk -= 1
267
+ self.state.scroll_offset = 0
268
+ else:
269
+ if self.state.selected_hunk < len(self.state.hunks) - 1:
270
+ self.state.selected_hunk += 1
271
+ self.state.scroll_offset = 0
272
+
273
+ elif c == 'k': # Up/previous hunk
274
+ if self.state.selected_hunk > 0:
275
+ self.state.selected_hunk -= 1
276
+ self.state.scroll_offset = 0
277
+
278
+ elif c == 'n': # Next hunk (same as j)
279
+ if self.state.selected_hunk < len(self.state.hunks) - 1:
280
+ self.state.selected_hunk += 1
281
+ self.state.scroll_offset = 0
282
+
283
+ elif c == 'p': # Previous hunk (same as k)
284
+ if self.state.selected_hunk > 0:
285
+ self.state.selected_hunk -= 1
286
+ self.state.scroll_offset = 0
287
+
288
+ elif c == ' ': # Scroll down within hunk
289
+ if self.state.hunks:
290
+ hunk = self.state.hunks[self.state.selected_hunk]
291
+ _, height = get_terminal_size()
292
+ content_height = height - 6
293
+ max_scroll = max(0, len(hunk.lines) - content_height)
294
+ self.state.scroll_offset = min(self.state.scroll_offset + 5, max_scroll)
295
+
296
+ elif c == 'b': # Scroll up within hunk
297
+ self.state.scroll_offset = max(0, self.state.scroll_offset - 5)
298
+
299
+ return True
300
+
301
+ def apply_decisions(self) -> str:
302
+ """Apply decisions and return the resulting content."""
303
+ if not self.state.hunks:
304
+ return self.state.modified
305
+
306
+ # If all hunks approved, return modified
307
+ if all(d == HunkDecision.APPROVED for d in self.state.decisions.values()):
308
+ return self.state.modified
309
+
310
+ # If all hunks rejected, return original
311
+ if all(d == HunkDecision.REJECTED for d in self.state.decisions.values()):
312
+ return self.state.original
313
+
314
+ # Partial application - reconstruct from decisions
315
+ # This is complex - for now, we'll use a simple approach:
316
+ # If any hunk is rejected, we need to carefully reconstruct
317
+
318
+ result_lines = self.state.original.splitlines(keepends=True)
319
+ offset = 0 # Track line number offset from applied changes
320
+
321
+ for i, hunk in enumerate(self.state.hunks):
322
+ if self.state.decisions[i] == HunkDecision.APPROVED:
323
+ # Apply this hunk
324
+ start = hunk.start_original - 1 + offset
325
+
326
+ # Count removals and additions in this hunk
327
+ removals = [l[1:] for l in hunk.lines if l.startswith('-')]
328
+ additions = [l[1:] for l in hunk.lines if l.startswith('+')]
329
+
330
+ # Remove old lines
331
+ del result_lines[start:start + len(removals)]
332
+
333
+ # Insert new lines
334
+ for j, line in enumerate(additions):
335
+ if not line.endswith('\n'):
336
+ line += '\n'
337
+ result_lines.insert(start + j, line)
338
+
339
+ # Update offset
340
+ offset += len(additions) - len(removals)
341
+
342
+ return ''.join(result_lines)
343
+
344
+ def run(self) -> Dict[str, any]:
345
+ """Run the interactive diff viewer. Returns approval decisions."""
346
+ if not HAS_TTY:
347
+ print("TTY not available - cannot run interactive diff viewer")
348
+ return {
349
+ "approved": False,
350
+ "decisions": {},
351
+ "content": self.state.original
352
+ }
353
+
354
+ if not self.state.hunks:
355
+ return {
356
+ "approved": True,
357
+ "decisions": {},
358
+ "content": self.state.modified
359
+ }
360
+
361
+ fd = sys.stdin.fileno()
362
+ old_settings = termios.tcgetattr(fd)
363
+
364
+ try:
365
+ tty.setcbreak(fd)
366
+ sys.stdout.write('\033[?25l') # Hide cursor
367
+
368
+ self.render_screen()
369
+
370
+ while True:
371
+ c = sys.stdin.read(1)
372
+ if not self.handle_input(c):
373
+ break
374
+ self.render_screen()
375
+
376
+ finally:
377
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
378
+ sys.stdout.write('\033[?25h') # Show cursor
379
+ sys.stdout.write('\033[2J\033[H') # Clear screen
380
+ sys.stdout.flush()
381
+
382
+ # Determine if approved
383
+ all_approved = all(d == HunkDecision.APPROVED for d in self.state.decisions.values())
384
+ any_approved = any(d == HunkDecision.APPROVED for d in self.state.decisions.values())
385
+
386
+ return {
387
+ "approved": any_approved,
388
+ "all_approved": all_approved,
389
+ "decisions": {i: d.value for i, d in self.state.decisions.items()},
390
+ "content": self.apply_decisions()
391
+ }
392
+
393
+
394
+ def show_diff_approval(file_path: str, original: str, modified: str) -> Dict[str, any]:
395
+ """
396
+ Show an interactive diff approval dialog.
397
+
398
+ Args:
399
+ file_path: Path to the file being edited
400
+ original: Original file content
401
+ modified: Modified file content
402
+
403
+ Returns:
404
+ Dict with:
405
+ - approved: bool - whether any changes were approved
406
+ - all_approved: bool - whether all changes were approved
407
+ - content: str - the resulting content after applying decisions
408
+ - decisions: dict - per-hunk decisions
409
+ """
410
+ viewer = DiffViewer(file_path, original, modified)
411
+ return viewer.run()
412
+
413
+
414
+ def quick_diff_preview(original: str, modified: str, max_lines: int = 20) -> str:
415
+ """
416
+ Generate a quick text-based diff preview (non-interactive).
417
+
418
+ Args:
419
+ original: Original content
420
+ modified: Modified content
421
+ max_lines: Maximum lines to show
422
+
423
+ Returns:
424
+ Colored diff string
425
+ """
426
+ original_lines = original.splitlines(keepends=True)
427
+ modified_lines = modified.splitlines(keepends=True)
428
+
429
+ diff = list(difflib.unified_diff(
430
+ original_lines,
431
+ modified_lines,
432
+ lineterm=''
433
+ ))
434
+
435
+ if not diff:
436
+ return "No changes"
437
+
438
+ result = []
439
+ for line in diff[:max_lines]:
440
+ if line.startswith('+') and not line.startswith('+++'):
441
+ result.append(f"\033[32m{line.rstrip()}\033[0m")
442
+ elif line.startswith('-') and not line.startswith('---'):
443
+ result.append(f"\033[31m{line.rstrip()}\033[0m")
444
+ elif line.startswith('@@'):
445
+ result.append(f"\033[36m{line.rstrip()}\033[0m")
446
+ else:
447
+ result.append(line.rstrip())
448
+
449
+ if len(diff) > max_lines:
450
+ result.append(f"\033[90m... ({len(diff) - max_lines} more lines)\033[0m")
451
+
452
+ return '\n'.join(result)
npcsh/execution.py CHANGED
@@ -4,7 +4,6 @@ Command execution utilities for npcsh
4
4
  import os
5
5
  import shutil
6
6
  import subprocess
7
- import sys
8
7
  from typing import List, Tuple, Any, Optional
9
8
 
10
9
  from termcolor import colored
npcsh/guac.py CHANGED
@@ -4,7 +4,6 @@ guac - Python data analysis mode CLI entry point
4
4
  This is a thin wrapper that executes the guac.jinx through the jinx mechanism.
5
5
  """
6
6
  import argparse
7
- import os
8
7
  import sys
9
8
 
10
9
  from npcsh._state import setup_shell
npcsh/mcp_helpers.py CHANGED
@@ -4,7 +4,6 @@ Raw MCP client with no exception handling and full visibility.
4
4
  """
5
5
 
6
6
  import asyncio
7
- import os
8
7
  import sys
9
8
  import json
10
9
  try:
@@ -245,7 +244,7 @@ class MCPClient:
245
244
 
246
245
 
247
246
  self._log(f"Executing tool: {tool_name} with args: {tool_args}")
248
- print(f"\nExecuting tool call:")
247
+ print("\nExecuting tool call:")
249
248
  print(f" Jinx name: {tool_name}")
250
249
  print(f" Jinx args: {tool_args}")
251
250
  print(f" Jinx args type: {type(tool_args)}")
@@ -268,7 +267,7 @@ class MCPClient:
268
267
  print(f" TextContent detected, text: {tool_result.text}")
269
268
  tool_result = tool_result.text
270
269
  elif isinstance(tool_result, list) and all(hasattr(item, 'text') for item in tool_result):
271
- print(f" List of TextContent detected")
270
+ print(" List of TextContent detected")
272
271
  tool_result = [item.text for item in tool_result]
273
272
 
274
273
 
npcsh/mcp_server.py CHANGED
@@ -7,9 +7,8 @@ npcpy.llm_funcs, and npcpy.npc_compiler as tools.
7
7
  import os
8
8
  import subprocess
9
9
  import json
10
- import asyncio
11
10
 
12
- from typing import Optional, Dict, Any, List, Union, Callable
11
+ from typing import List, Callable
13
12
 
14
13
  from mcp.server.fastmcp import FastMCP
15
14
  import importlib
@@ -20,19 +19,15 @@ from sqlalchemy import text
20
19
  import os
21
20
  import subprocess
22
21
  import json
23
- import asyncio
24
22
  try:
25
23
  import inspect
26
24
  except:
27
25
  pass
28
- from typing import Optional, Dict, Any, List, Union, Callable, get_type_hints
26
+ from typing import List, Callable
29
27
 
30
28
  from functools import wraps
31
- import sys
32
29
 
33
- from npcpy.llm_funcs import generate_group_candidates, abstract,
34
- zoom_in, execute_llm_command, gen_image
35
- from npcpy.memory.search import search_similar_texts, execute_search_command, execute_rag_command, answer_with_rag, execute_brainblast_command
30
+ from npcpy.llm_funcs import (gen_image)
36
31
  from npcpy.data.load import load_file_contents
37
32
  from npcpy.memory.command_history import CommandHistory
38
33
  from npcpy.data.image import capture_screenshot
@@ -268,7 +263,7 @@ def register_selected_npcpy_tools():
268
263
  gen_image,
269
264
  load_file_contents,
270
265
  capture_screenshot,
271
- search_web, ]
266
+ search_web ]
272
267
 
273
268
  for func in tools:
274
269
 
@@ -293,7 +288,7 @@ register_selected_npcpy_tools()
293
288
 
294
289
 
295
290
  if __name__ == "__main__":
296
- print(f"Starting enhanced NPCPY MCP server...")
291
+ print("Starting enhanced NPCPY MCP server...")
297
292
  print(f"Workspace: {DEFAULT_WORKSPACE}")
298
293
 
299
294
 
npcsh/npc.py CHANGED
@@ -7,7 +7,6 @@ from typing import Optional
7
7
  from npcsh._state import (
8
8
  NPCSH_CHAT_MODEL,
9
9
  NPCSH_CHAT_PROVIDER,
10
- NPCSH_API_URL,
11
10
  NPCSH_DB_PATH,
12
11
  NPCSH_STREAM_OUTPUT,
13
12
  initial_state,
@@ -16,9 +15,8 @@ from npcpy.npc_sysenv import (
16
15
  print_and_process_stream_with_markdown,
17
16
  render_markdown,
18
17
  )
19
- from npcpy.npc_compiler import NPC, Team
18
+ from npcpy.npc_compiler import NPC
20
19
  from npcsh.routes import router
21
- from npcpy.llm_funcs import check_llm_command
22
20
  from sqlalchemy import create_engine
23
21
 
24
22
  from npcsh._state import (
@@ -256,8 +254,9 @@ def main():
256
254
  print(
257
255
  f"Processing prompt: '{prompt}' with NPC: '{args.npc}'..."
258
256
  )
259
-
260
- shell_state.current_mode = 'chat'
257
+
258
+ # Use NPCSH_DEFAULT_MODE environment variable, default to 'agent' for tool execution
259
+ shell_state.current_mode = os.environ.get('NPCSH_DEFAULT_MODE', 'agent')
261
260
  updated_state, result = execute_command(
262
261
  prompt,
263
262
  shell_state,
@@ -274,12 +273,12 @@ def main():
274
273
  )
275
274
 
276
275
  if (
277
- hasattr(output, '__iter__')
276
+ hasattr(output, '__iter__')
278
277
  and not isinstance(output, (str, bytes, dict, list))
279
278
  ):
280
- final_output = print_and_process_stream_with_markdown(
281
- output,
282
- model_for_stream,
279
+ print_and_process_stream_with_markdown(
280
+ output,
281
+ model_for_stream,
283
282
  provider_for_stream,
284
283
  show=True
285
284
  )
@@ -289,7 +288,7 @@ def main():
289
288
  hasattr(result, '__iter__')
290
289
  and not isinstance(result, (str, bytes, dict, list))
291
290
  ):
292
- final_output = print_and_process_stream_with_markdown(
291
+ print_and_process_stream_with_markdown(
293
292
  result,
294
293
  effective_model,
295
294
  effective_provider,
@@ -324,7 +323,7 @@ def jinx_main():
324
323
  if arg in ['-h', '--help']:
325
324
  print(f"Usage: {jinx_name} [key=value ...]")
326
325
  print(f"\nRun the '{jinx_name}' jinx with specified parameters.")
327
- print(f"\nExamples:")
326
+ print("\nExamples:")
328
327
  print(f" {jinx_name} show=1")
329
328
  print(f" {jinx_name} model=my_model db=~/mydb.db")
330
329
  print(f"\nOr use: npc {jinx_name} [key=value ...]")
@@ -21,7 +21,7 @@ steps:
21
21
  npc_name_input = {{ npc_name | default("") | tojson }}.strip() or None
22
22
 
23
23
  if not model:
24
- model = npc.model if npc and npc.model
24
+ model = npc.model if npc and npc.model else ""
25
25
  if not provider:
26
26
  provider = npc.provider if npc and npc.provider else "anthropic"
27
27