npcsh 1.1.17__py3-none-any.whl → 1.1.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. npcsh/_state.py +114 -91
  2. npcsh/alicanto.py +2 -2
  3. npcsh/benchmark/__init__.py +8 -2
  4. npcsh/benchmark/npcsh_agent.py +46 -12
  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 +1 -3
  10. npcsh/conversation_viewer.py +389 -0
  11. npcsh/corca.py +0 -1
  12. npcsh/execution.py +0 -1
  13. npcsh/guac.py +0 -1
  14. npcsh/mcp_helpers.py +2 -3
  15. npcsh/mcp_server.py +5 -10
  16. npcsh/npc.py +10 -11
  17. npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
  18. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
  19. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
  20. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
  21. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
  22. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
  23. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
  24. npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
  25. npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
  26. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
  27. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
  28. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
  29. npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
  30. npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
  31. npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
  32. npcsh/npc_team/jinxs/modes/guac.jinx +544 -0
  33. npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
  34. npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
  35. npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
  36. npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
  37. npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
  38. npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
  39. npcsh/npcsh.py +7 -4
  40. npcsh/plonk.py +0 -1
  41. npcsh/pti.py +0 -1
  42. npcsh/routes.py +1 -3
  43. npcsh/spool.py +0 -1
  44. npcsh/ui.py +0 -1
  45. npcsh/wander.py +0 -1
  46. npcsh/yap.py +0 -1
  47. npcsh-1.1.18.data/data/npcsh/npc_team/alicanto.jinx +356 -0
  48. npcsh-1.1.18.data/data/npcsh/npc_team/arxiv.jinx +720 -0
  49. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
  50. npcsh-1.1.18.data/data/npcsh/npc_team/corca.jinx +430 -0
  51. npcsh-1.1.18.data/data/npcsh/npc_team/db_search.jinx +348 -0
  52. npcsh-1.1.18.data/data/npcsh/npc_team/file_search.jinx +339 -0
  53. npcsh-1.1.18.data/data/npcsh/npc_team/guac.jinx +544 -0
  54. npcsh-1.1.18.data/data/npcsh/npc_team/jinxs.jinx +331 -0
  55. npcsh-1.1.18.data/data/npcsh/npc_team/kg_search.jinx +418 -0
  56. npcsh-1.1.18.data/data/npcsh/npc_team/mem_review.jinx +73 -0
  57. npcsh-1.1.18.data/data/npcsh/npc_team/mem_search.jinx +388 -0
  58. npcsh-1.1.18.data/data/npcsh/npc_team/paper_search.jinx +412 -0
  59. npcsh-1.1.18.data/data/npcsh/npc_team/plonk.jinx +379 -0
  60. npcsh-1.1.18.data/data/npcsh/npc_team/pti.jinx +357 -0
  61. npcsh-1.1.18.data/data/npcsh/npc_team/reattach.jinx +291 -0
  62. npcsh-1.1.18.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
  63. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sleep.jinx +22 -11
  64. npcsh-1.1.18.data/data/npcsh/npc_team/spool.jinx +350 -0
  65. npcsh-1.1.18.data/data/npcsh/npc_team/sql.jinx +20 -0
  66. npcsh-1.1.18.data/data/npcsh/npc_team/wander.jinx +455 -0
  67. npcsh-1.1.18.data/data/npcsh/npc_team/web_search.jinx +283 -0
  68. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.jinx +13 -7
  69. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/METADATA +90 -1
  70. npcsh-1.1.18.dist-info/RECORD +235 -0
  71. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/WHEEL +1 -1
  72. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/entry_points.txt +0 -3
  73. npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
  74. npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
  75. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
  76. npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
  77. npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
  78. npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
  79. npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
  80. npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
  81. npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
  82. npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
  83. npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
  84. npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
  85. npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
  86. npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
  87. npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
  88. npcsh-1.1.17.dist-info/RECORD +0 -219
  89. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  90. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  91. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/alicanto.png +0 -0
  92. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  93. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  94. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/build.jinx +0 -0
  95. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/chat.jinx +0 -0
  96. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/click.jinx +0 -0
  97. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  98. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  99. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  100. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  101. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compile.jinx +0 -0
  102. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/compress.jinx +0 -0
  103. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  104. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/convene.jinx +0 -0
  105. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.npc +0 -0
  106. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca.png +0 -0
  107. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/corca_example.png +0 -0
  108. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  109. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  110. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  111. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic.npc +0 -0
  112. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/frederic4.png +0 -0
  113. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.npc +0 -0
  114. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/guac.png +0 -0
  115. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/help.jinx +0 -0
  116. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  117. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/init.jinx +0 -0
  118. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  119. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  120. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  121. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  122. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  123. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  124. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  125. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/notify.jinx +0 -0
  126. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  127. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  128. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/nql.jinx +0 -0
  129. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  130. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  131. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/ots.jinx +0 -0
  132. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/paste.jinx +0 -0
  133. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.npc +0 -0
  134. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonk.png +0 -0
  135. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  136. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  137. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/python.jinx +0 -0
  138. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  139. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/roll.jinx +0 -0
  140. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  141. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sample.jinx +0 -0
  142. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  143. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/search.jinx +0 -0
  144. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  145. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/serve.jinx +0 -0
  146. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/set.jinx +0 -0
  147. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sh.jinx +0 -0
  148. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/shh.jinx +0 -0
  149. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  150. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  152. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/spool.png +0 -0
  153. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch.jinx +0 -0
  154. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  155. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  156. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  159. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  160. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  161. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/usage.jinx +0 -0
  162. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  163. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  164. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/wait.jinx +0 -0
  165. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  166. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/yap.png +0 -0
  167. {npcsh-1.1.17.data → npcsh-1.1.18.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  168. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/licenses/LICENSE +0 -0
  169. {npcsh-1.1.17.dist-info → npcsh-1.1.18.dist-info}/top_level.txt +0 -0
@@ -1,94 +1,339 @@
1
1
  jinx_name: file_search
2
- description: Search file contents using RAG (Retrieval Augmented Generation)
2
+ description: Find and browse files with interactive TUI
3
3
  inputs:
4
- - query: ""
5
- - file_paths: ""
6
- - emodel: ""
7
- - eprovider: ""
8
- - vector_db_path: ""
9
- - recursive: "false"
4
+ - pattern: ""
5
+ - path: "."
6
+ - recursive: "true"
7
+ - text: "false"
10
8
 
11
9
  steps:
12
10
  - name: search_files
13
11
  engine: python
14
12
  code: |
15
13
  import os
14
+ import sys
15
+ import tty
16
+ import termios
16
17
  import glob as globmod
18
+ import subprocess
19
+ import fnmatch
20
+ from datetime import datetime
17
21
 
18
- query = context.get('query', '').strip()
19
- file_paths_str = context.get('file_paths', '').strip()
22
+ pattern = context.get('pattern', '').strip()
23
+ base_path = context.get('path', '.').strip() or '.'
24
+ recursive = context.get('recursive', 'true').lower() in ('true', '1', 'yes')
25
+ text_mode = context.get('text', '').lower() in ('true', '1', 'yes')
20
26
 
21
- if not query or not file_paths_str:
27
+ if not pattern:
22
28
  lines = [
23
- "Usage: /file_search <query> file_paths=<path1,path2,...>",
29
+ "Usage: /file_search <pattern>",
24
30
  "",
25
31
  "Options:",
26
- " file_paths - Comma-separated file paths or glob patterns (required)",
27
- " emodel - Embedding model",
28
- " eprovider - Embedding provider",
29
- " vector_db_path - Path to vector database",
30
- " recursive - Use recursive glob for patterns (default false)",
32
+ " path - Base directory to search (default: current dir)",
33
+ " recursive - Search subdirectories (default: true)",
34
+ " text - Text-only output, no TUI (true/false)",
35
+ "",
36
+ "TUI Controls:",
37
+ " j/k or arrows - Navigate",
38
+ " 1/2/3 - Sort by name/size/date",
39
+ " p - Preview file contents",
40
+ " o - Open in $EDITOR",
41
+ " i - Open in incognide",
42
+ " c - Copy path to clipboard",
43
+ " q/ESC - Quit",
31
44
  "",
32
45
  "Examples:",
33
- " /file_search how does auth work file_paths=src/*.py",
34
- " /file_search database schema file_paths=docs/,README.md",
46
+ " /file_search *.py",
47
+ " /file_search *.js path=src",
48
+ " /file_search test_*.py recursive=false",
35
49
  ]
36
50
  context['output'] = "\n".join(lines)
37
51
  else:
38
- recursive = context.get('recursive', 'false').lower() == 'true'
39
- emodel = context.get('emodel') or None
40
- eprovider = context.get('eprovider') or None
41
- try:
42
- emodel = emodel or (state.embedding_model if 'state' in dir() and state else None)
43
- eprovider = eprovider or (state.embedding_provider if 'state' in dir() and state else None)
44
- except:
45
- pass
46
- vector_db_path = context.get('vector_db_path') or os.path.expanduser("~/.npcsh/npcsh_chroma.db")
52
+ base_path = os.path.expanduser(base_path)
53
+ if not os.path.isabs(base_path):
54
+ base_path = os.path.abspath(base_path)
47
55
 
48
56
  try:
49
- resolved_paths = []
50
- for path_spec in file_paths_str.split(','):
51
- path_spec = path_spec.strip()
52
- if not path_spec:
53
- continue
54
- expanded = os.path.expanduser(path_spec)
55
- if '*' in expanded or '?' in expanded:
56
- if recursive:
57
- matches = globmod.glob(expanded, recursive=True)
58
- else:
59
- matches = globmod.glob(expanded)
60
- resolved_paths.extend(matches)
61
- else:
62
- resolved_paths.append(os.path.abspath(expanded))
63
-
64
- file_contents = []
65
- loaded_files = []
66
- for path in resolved_paths:
67
- if os.path.isfile(path):
68
- chunks = load_file_contents(path)
69
- basename = os.path.basename(path)
70
- file_contents.extend([basename + ": " + chunk for chunk in chunks])
71
- loaded_files.append(basename)
72
- elif os.path.isdir(path):
73
- for root, dirs, files in os.walk(path):
74
- for f in files:
57
+ # Find matching files
58
+ files = []
59
+
60
+ if recursive:
61
+ # Walk directory tree
62
+ for root, dirs, filenames in os.walk(base_path):
63
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
64
+ for f in filenames:
65
+ if fnmatch.fnmatch(f, pattern) or fnmatch.fnmatch(f.lower(), pattern.lower()):
75
66
  fpath = os.path.join(root, f)
76
- chunks = load_file_contents(fpath)
77
- file_contents.extend([f + ": " + chunk for chunk in chunks])
78
- loaded_files.append(f)
67
+ try:
68
+ stat = os.stat(fpath)
69
+ files.append({
70
+ 'name': f,
71
+ 'path': fpath,
72
+ 'size': stat.st_size,
73
+ 'mtime': stat.st_mtime
74
+ })
75
+ except:
76
+ pass
77
+ else:
78
+ # Just current directory
79
+ for f in os.listdir(base_path):
80
+ if fnmatch.fnmatch(f, pattern) or fnmatch.fnmatch(f.lower(), pattern.lower()):
81
+ fpath = os.path.join(base_path, f)
82
+ if os.path.isfile(fpath):
83
+ try:
84
+ stat = os.stat(fpath)
85
+ files.append({
86
+ 'name': f,
87
+ 'path': fpath,
88
+ 'size': stat.st_size,
89
+ 'mtime': stat.st_mtime
90
+ })
91
+ except:
92
+ pass
79
93
 
80
- if not file_contents:
81
- context['output'] = "No files found or loaded from: " + file_paths_str
94
+ if not files:
95
+ context['output'] = f"No files matching '{pattern}' in {base_path}"
96
+ elif text_mode:
97
+ # Text-only output
98
+ lines = [f"Found {len(files)} files matching '{pattern}':", ""]
99
+ for f in sorted(files, key=lambda x: x['name']):
100
+ lines.append(f" {f['path']}")
101
+ context['output'] = "\n".join(lines)
82
102
  else:
83
- result = execute_rag_command(
84
- command=query,
85
- vector_db_path=vector_db_path,
86
- embedding_model=emodel,
87
- embedding_provider=eprovider,
88
- file_contents=file_contents
89
- )
90
- response = result.get('response', 'No RAG response.')
91
- context['output'] = "Searched " + str(len(loaded_files)) + " files:\n\n" + response
103
+ # Interactive TUI mode
104
+ def get_terminal_size():
105
+ try:
106
+ size = os.get_terminal_size()
107
+ return size.columns, size.lines
108
+ except:
109
+ return 80, 24
110
+
111
+ def format_size(size):
112
+ if size < 1024:
113
+ return f"{size}B"
114
+ elif size < 1024 * 1024:
115
+ return f"{size // 1024}K"
116
+ else:
117
+ return f"{size // (1024 * 1024)}M"
118
+
119
+ def format_date(mtime):
120
+ try:
121
+ dt = datetime.fromtimestamp(mtime)
122
+ now = datetime.now()
123
+ diff = now - dt
124
+ if diff.days == 0:
125
+ return dt.strftime('%H:%M')
126
+ elif diff.days < 7:
127
+ return dt.strftime('%a %H:%M')
128
+ else:
129
+ return dt.strftime('%b %d')
130
+ except:
131
+ return '?'
132
+
133
+ width, height = get_terminal_size()
134
+ selected = 0
135
+ scroll = 0
136
+ list_height = height - 5
137
+ mode = 'list'
138
+ preview_scroll = 0
139
+ preview_lines = []
140
+ sort_mode = 'name' # name, size, date
141
+
142
+ def sort_files(files, sort_mode):
143
+ if sort_mode == 'name':
144
+ return sorted(files, key=lambda x: x['name'].lower())
145
+ elif sort_mode == 'size':
146
+ return sorted(files, key=lambda x: x['size'], reverse=True)
147
+ elif sort_mode == 'date':
148
+ return sorted(files, key=lambda x: x['mtime'], reverse=True)
149
+ return files
150
+
151
+ display_files = sort_files(files, sort_mode)
152
+
153
+ fd = sys.stdin.fileno()
154
+ old_settings = termios.tcgetattr(fd)
155
+
156
+ try:
157
+ tty.setcbreak(fd)
158
+ sys.stdout.write('\033[?25l')
159
+ sys.stdout.write('\033[2J\033[H')
160
+
161
+ while True:
162
+ width, height = get_terminal_size()
163
+ list_height = height - 5
164
+
165
+ if mode == 'list':
166
+ if selected < scroll:
167
+ scroll = selected
168
+ elif selected >= scroll + list_height:
169
+ scroll = selected - list_height + 1
170
+
171
+ sys.stdout.write('\033[H')
172
+
173
+ # Header
174
+ if mode == 'list':
175
+ sort_ind = {'name': '1', 'size': '2', 'date': '3'}[sort_mode]
176
+ header = f" FILES ({len(display_files)}): '{pattern}' [sort:{sort_mode}({sort_ind})] "
177
+ else:
178
+ header = f" PREVIEW: {display_files[selected]['name']} "
179
+ sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
180
+
181
+ # Column headers
182
+ if mode == 'list':
183
+ col_header = f' {"NAME":<30} {"SIZE":<8} {"MODIFIED":<12} {"PATH":<30}'
184
+ sys.stdout.write(f'\033[90m{col_header[:width]}\033[0m\n')
185
+ else:
186
+ sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
187
+
188
+ if mode == 'list':
189
+ for i in range(list_height):
190
+ idx = scroll + i
191
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
192
+ if idx >= len(display_files):
193
+ continue
194
+
195
+ f = display_files[idx]
196
+ name = f['name'][:30]
197
+ size = format_size(f['size'])
198
+ mtime = format_date(f['mtime'])
199
+ # Show relative path if possible
200
+ path = f['path']
201
+ cwd = os.getcwd()
202
+ if path.startswith(cwd):
203
+ path = '.' + path[len(cwd):]
204
+ path = path[:35]
205
+
206
+ line = f" {name:<30} {size:<8} {mtime:<12} {path}"
207
+ line = line[:width-1]
208
+
209
+ if idx == selected:
210
+ sys.stdout.write(f'\033[47;30;1m>{line}\033[0m')
211
+ else:
212
+ sys.stdout.write(f' {line}')
213
+
214
+ # Status bar
215
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
216
+ f = display_files[selected] if display_files else {}
217
+ full_path = f.get('path', '')
218
+ sys.stdout.write(f'\033[{height-1};1H\033[K {full_path}'.ljust(width)[:width])
219
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav 1/2/3:Sort p:Preview o:Edit i:Incog c:Copy q:Quit [{selected+1}/{len(display_files)}] \033[0m')
220
+
221
+ elif mode == 'preview':
222
+ for i in range(list_height):
223
+ idx = preview_scroll + i
224
+ sys.stdout.write(f'\033[{3+i};1H\033[K')
225
+ if idx < len(preview_lines):
226
+ sys.stdout.write(preview_lines[idx][:width-1])
227
+
228
+ sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
229
+ sys.stdout.write(f'\033[{height-1};1H\033[K [{preview_scroll+1}/{len(preview_lines)} lines]')
230
+ sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back o:Edit q:Quit \033[0m')
231
+
232
+ sys.stdout.flush()
233
+
234
+ c = sys.stdin.read(1)
235
+
236
+ if c == '\x1b':
237
+ c2 = sys.stdin.read(1)
238
+ if c2 == '[':
239
+ c3 = sys.stdin.read(1)
240
+ if c3 == 'A':
241
+ if mode == 'list' and selected > 0:
242
+ selected -= 1
243
+ elif mode == 'preview' and preview_scroll > 0:
244
+ preview_scroll -= 1
245
+ elif c3 == 'B':
246
+ if mode == 'list' and selected < len(display_files) - 1:
247
+ selected += 1
248
+ elif mode == 'preview' and preview_scroll < 500:
249
+ preview_scroll += 1
250
+ else:
251
+ if mode == 'preview':
252
+ mode = 'list'
253
+ sys.stdout.write('\033[2J\033[H')
254
+ else:
255
+ context['output'] = "Cancelled."
256
+ break
257
+ continue
258
+
259
+ if c == 'q' or c == '\x03':
260
+ context['output'] = "Cancelled."
261
+ break
262
+ elif c == 'k':
263
+ if mode == 'list' and selected > 0:
264
+ selected -= 1
265
+ elif mode == 'preview' and preview_scroll > 0:
266
+ preview_scroll -= 1
267
+ elif c == 'j':
268
+ if mode == 'list' and selected < len(display_files) - 1:
269
+ selected += 1
270
+ elif mode == 'preview' and preview_scroll < 500:
271
+ preview_scroll += 1
272
+ elif c == '1':
273
+ sort_mode = 'name'
274
+ display_files = sort_files(files, sort_mode)
275
+ selected = 0
276
+ scroll = 0
277
+ elif c == '2':
278
+ sort_mode = 'size'
279
+ display_files = sort_files(files, sort_mode)
280
+ selected = 0
281
+ scroll = 0
282
+ elif c == '3':
283
+ sort_mode = 'date'
284
+ display_files = sort_files(files, sort_mode)
285
+ selected = 0
286
+ scroll = 0
287
+ elif c == 'p' and mode == 'list' and display_files:
288
+ # Preview file contents
289
+ f = display_files[selected]
290
+ try:
291
+ with open(f['path'], 'r', errors='replace') as fp:
292
+ content = fp.read(50000)
293
+ preview_lines = content.split('\n')[:500]
294
+ except:
295
+ preview_lines = ['Error reading file']
296
+ mode = 'preview'
297
+ preview_scroll = 0
298
+ sys.stdout.write('\033[2J\033[H')
299
+ elif c == 'b' and mode == 'preview':
300
+ mode = 'list'
301
+ sys.stdout.write('\033[2J\033[H')
302
+ elif c == 'o' and display_files:
303
+ f = display_files[selected]
304
+ editor = os.environ.get('EDITOR', 'vim')
305
+ # Restore terminal, run editor, then back to TUI
306
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
307
+ sys.stdout.write('\033[?25h\033[2J\033[H')
308
+ os.system(f'{editor} "{f["path"]}"')
309
+ tty.setcbreak(fd)
310
+ sys.stdout.write('\033[?25l\033[2J\033[H')
311
+ elif c == 'i' and display_files:
312
+ f = display_files[selected]
313
+ try:
314
+ subprocess.run(['npcsh', '-c', f'/navigate url=file://{f["path"]}'], check=False, capture_output=True)
315
+ except:
316
+ pass
317
+ elif c == 'c' and display_files:
318
+ f = display_files[selected]
319
+ try:
320
+ subprocess.run(['xclip', '-selection', 'clipboard'], input=f['path'].encode(), check=True)
321
+ except:
322
+ try:
323
+ subprocess.run(['xsel', '--clipboard', '--input'], input=f['path'].encode(), check=True)
324
+ except:
325
+ pass
326
+ elif c in ('\r', '\n') and display_files:
327
+ f = display_files[selected]
328
+ context['output'] = f"Selected: {f['name']}\nPath: {f['path']}"
329
+ break
330
+
331
+ finally:
332
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
333
+ sys.stdout.write('\033[?25h')
334
+ sys.stdout.write('\033[2J\033[H')
335
+ sys.stdout.flush()
336
+
92
337
  except Exception as e:
93
338
  import traceback
94
339
  context['output'] = "File search error: " + str(e) + "\n" + traceback.format_exc()