npcsh 1.1.20__tar.gz → 1.1.21__tar.gz

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 (161) hide show
  1. {npcsh-1.1.20/npcsh.egg-info → npcsh-1.1.21}/PKG-INFO +2 -2
  2. {npcsh-1.1.20 → npcsh-1.1.21}/README.md +1 -1
  3. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/_state.py +5 -71
  4. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/diff_viewer.py +3 -3
  5. npcsh-1.1.21/npcsh/npc_team/jinxs/lib/core/compress.jinx +428 -0
  6. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +17 -6
  7. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +17 -6
  8. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +19 -8
  9. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +52 -14
  10. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/lib/utils}/benchmark.jinx +2 -2
  11. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/lib/utils}/jinxs.jinx +12 -12
  12. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/lib/utils}/models.jinx +7 -7
  13. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/lib/utils}/setup.jinx +6 -6
  14. npcsh-1.1.21/npcsh/npc_team/jinxs/modes/alicanto.jinx +1633 -0
  15. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/modes/arxiv.jinx +5 -5
  16. npcsh-1.1.21/npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  17. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/modes/corca.jinx +3 -3
  18. npcsh-1.1.21/npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  19. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/modes}/kg.jinx +13 -13
  20. npcsh-1.1.21/npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  21. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/modes}/nql.jinx +10 -21
  22. npcsh-1.1.21/npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  23. npcsh-1.1.21/npcsh/npc_team/jinxs/modes/plonk.jinx +565 -0
  24. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/modes/reattach.jinx +3 -3
  25. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/modes/spool.jinx +3 -3
  26. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/modes}/team.jinx +12 -12
  27. npcsh-1.1.21/npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  28. npcsh-1.1.21/npcsh/npc_team/jinxs/modes/wander.jinx +728 -0
  29. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/modes/yap.jinx +10 -3
  30. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npcsh.py +112 -47
  31. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/routes.py +4 -1
  32. npcsh-1.1.21/npcsh/salmon_simulation.py +0 -0
  33. {npcsh-1.1.20 → npcsh-1.1.21/npcsh.egg-info}/PKG-INFO +2 -2
  34. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh.egg-info/SOURCES.txt +18 -20
  35. npcsh-1.1.21/npcsh.egg-info/entry_points.txt +11 -0
  36. {npcsh-1.1.20 → npcsh-1.1.21}/setup.py +1 -1
  37. npcsh-1.1.20/npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -300
  38. npcsh-1.1.20/npcsh/npc_team/jinxs/bin/memories.jinx +0 -317
  39. npcsh-1.1.20/npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  40. npcsh-1.1.20/npcsh/npc_team/jinxs/lib/core/compress.jinx +0 -140
  41. npcsh-1.1.20/npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  42. npcsh-1.1.20/npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  43. npcsh-1.1.20/npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  44. npcsh-1.1.20/npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  45. npcsh-1.1.20/npcsh/npc_team/jinxs/modes/alicanto.jinx +0 -356
  46. npcsh-1.1.20/npcsh/npc_team/jinxs/modes/plonk.jinx +0 -379
  47. npcsh-1.1.20/npcsh/npc_team/jinxs/modes/wander.jinx +0 -455
  48. npcsh-1.1.20/npcsh/npc_team/plonkjr.npc +0 -23
  49. npcsh-1.1.20/npcsh.egg-info/entry_points.txt +0 -25
  50. {npcsh-1.1.20 → npcsh-1.1.21}/LICENSE +0 -0
  51. {npcsh-1.1.20 → npcsh-1.1.21}/MANIFEST.in +0 -0
  52. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/__init__.py +0 -0
  53. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/alicanto.py +0 -0
  54. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/benchmark/__init__.py +0 -0
  55. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/benchmark/npcsh_agent.py +0 -0
  56. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/benchmark/runner.py +0 -0
  57. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/benchmark/templates/install-npcsh.sh.j2 +0 -0
  58. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/build.py +0 -0
  59. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/completion.py +0 -0
  60. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/config.py +0 -0
  61. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/conversation_viewer.py +0 -0
  62. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/corca.py +0 -0
  63. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/execution.py +0 -0
  64. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/guac.py +0 -0
  65. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/mcp_helpers.py +0 -0
  66. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/mcp_server.py +0 -0
  67. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc.py +0 -0
  68. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/alicanto.npc +0 -0
  69. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/alicanto.png +0 -0
  70. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/corca.npc +0 -0
  71. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/corca.png +0 -0
  72. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/corca_example.png +0 -0
  73. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/frederic.npc +0 -0
  74. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/frederic4.png +0 -0
  75. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/guac.npc +0 -0
  76. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/guac.png +0 -0
  77. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -0
  78. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -0
  79. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -0
  80. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -0
  81. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -0
  82. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/incognide.jinx +0 -0
  83. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -0
  84. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -0
  85. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/notify.jinx +0 -0
  86. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -0
  87. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -0
  88. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -0
  89. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -0
  90. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -0
  91. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -0
  92. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -0
  93. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -0
  94. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -0
  95. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +0 -0
  96. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +0 -0
  97. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/browser/close_browser.jinx +0 -0
  98. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +0 -0
  99. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/computer_use/click.jinx +0 -0
  100. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +0 -0
  101. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +0 -0
  102. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +0 -0
  103. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/computer_use/trigger.jinx +0 -0
  104. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +0 -0
  105. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +0 -0
  106. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/chat.jinx +0 -0
  107. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/cmd.jinx +0 -0
  108. {npcsh-1.1.20/npcsh/npc_team/jinxs/lib/orchestration → npcsh-1.1.21/npcsh/npc_team/jinxs/lib/core}/convene.jinx +0 -0
  109. {npcsh-1.1.20/npcsh/npc_team/jinxs/lib/orchestration → npcsh-1.1.21/npcsh/npc_team/jinxs/lib/core}/delegate.jinx +0 -0
  110. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/edit_file.jinx +0 -0
  111. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/load_file.jinx +0 -0
  112. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/ots.jinx +0 -0
  113. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/paste.jinx +0 -0
  114. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/python.jinx +0 -0
  115. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/lib/core}/sample.jinx +0 -0
  116. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/search.jinx +0 -0
  117. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/sh.jinx +0 -0
  118. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/sleep.jinx +0 -0
  119. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/core/sql.jinx +0 -0
  120. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -0
  121. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/compile.jinx +0 -0
  122. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/help.jinx +0 -0
  123. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/init.jinx +0 -0
  124. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/serve.jinx +0 -0
  125. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/set.jinx +0 -0
  126. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/shh.jinx +0 -0
  127. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/switch.jinx +0 -0
  128. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/switches.jinx +0 -0
  129. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/lib/utils}/sync.jinx +0 -0
  130. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +0 -0
  131. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/usage.jinx +0 -0
  132. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/lib/utils/verbose.jinx +0 -0
  133. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/modes/guac.jinx +0 -0
  134. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/jinxs/modes/pti.jinx +0 -0
  135. {npcsh-1.1.20/npcsh/npc_team/jinxs/bin → npcsh-1.1.21/npcsh/npc_team/jinxs/modes}/roll.jinx +0 -0
  136. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/kadiefa.npc +0 -0
  137. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/kadiefa.png +0 -0
  138. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/npcsh.ctx +0 -0
  139. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/npcsh_sibiji.png +0 -0
  140. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/plonk.npc +0 -0
  141. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/plonk.png +0 -0
  142. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/plonkjr.png +0 -0
  143. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/sibiji.npc +0 -0
  144. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/sibiji.png +0 -0
  145. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/spool.png +0 -0
  146. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/npc_team/yap.png +0 -0
  147. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/parsing.py +0 -0
  148. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/plonk.py +0 -0
  149. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/pti.py +0 -0
  150. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/spool.py +0 -0
  151. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/ui.py +0 -0
  152. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/wander.py +0 -0
  153. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh/yap.py +0 -0
  154. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh.egg-info/dependency_links.txt +0 -0
  155. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh.egg-info/requires.txt +0 -0
  156. {npcsh-1.1.20 → npcsh-1.1.21}/npcsh.egg-info/top_level.txt +0 -0
  157. {npcsh-1.1.20 → npcsh-1.1.21}/project/__init__.py +0 -0
  158. {npcsh-1.1.20 → npcsh-1.1.21}/setup.cfg +0 -0
  159. {npcsh-1.1.20 → npcsh-1.1.21}/tests/test_config.py +0 -0
  160. {npcsh-1.1.20 → npcsh-1.1.21}/tests/test_jinxs.py +0 -0
  161. {npcsh-1.1.20 → npcsh-1.1.21}/tests/test_tool_routing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcsh
3
- Version: 1.1.20
3
+ Version: 1.1.21
4
4
  Summary: npcsh is a command-line toolkit for using AI agents in novel ways.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcsh
6
6
  Author: Christopher Agostino
@@ -108,7 +108,7 @@ Dynamic: summary
108
108
 
109
109
  # npcsh
110
110
 
111
- The NPC shell (`npcsh`) makes the most of multi-modal LLMs and agents through a powerful set of simple slash commands and novel interactive modes, all from the comfort of the command line. Build teams of agents and schedule them on jobs, engineer context, and design custom interaction modes and Jinja Execution templates (Jinxs for you and your agents to invoke, all managed scalably for organizations of any size through the NPC data layer.
111
+ The NPC shell (`npcsh`) makes the most of multi-modal LLMs and agents through a powerful set of simple slash commands and novel interactive modes, all from the comfort of the command line. Build teams of agents and schedule them on jobs, engineer context, and design custom interaction modes and Jinja Execution templates (Jinxs for you and your agents to invoke, all managed scalably for organizations of any size through the NPC data layer.
112
112
 
113
113
  To get started:
114
114
  For users who want to mainly use models through APIs (`ollama`, `gemini`, `kimi`, `grok`, `deepseek`, `anthropic`, `openai`, `mistral`, or any others provided by litellm )
@@ -5,7 +5,7 @@
5
5
 
6
6
  # npcsh
7
7
 
8
- The NPC shell (`npcsh`) makes the most of multi-modal LLMs and agents through a powerful set of simple slash commands and novel interactive modes, all from the comfort of the command line. Build teams of agents and schedule them on jobs, engineer context, and design custom interaction modes and Jinja Execution templates (Jinxs for you and your agents to invoke, all managed scalably for organizations of any size through the NPC data layer.
8
+ The NPC shell (`npcsh`) makes the most of multi-modal LLMs and agents through a powerful set of simple slash commands and novel interactive modes, all from the comfort of the command line. Build teams of agents and schedule them on jobs, engineer context, and design custom interaction modes and Jinja Execution templates (Jinxs for you and your agents to invoke, all managed scalably for organizations of any size through the NPC data layer.
9
9
 
10
10
  To get started:
11
11
  For users who want to mainly use models through APIs (`ollama`, `gemini`, `kimi`, `grok`, `deepseek`, `anthropic`, `openai`, `mistral`, or any others provided by litellm )
@@ -3444,9 +3444,9 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3444
3444
  command_history = CommandHistory(db_path)
3445
3445
 
3446
3446
  if not is_npcsh_initialized():
3447
- print("Initializing NPCSH...")
3447
+ print("Setting up npcsh for first use...")
3448
3448
  initialize_base_npcs_if_needed(db_path)
3449
- print("NPCSH initialization complete. Restart or source ~/.npcshrc.")
3449
+ print("Setup complete.")
3450
3450
 
3451
3451
  try:
3452
3452
  setup_readline()
@@ -3458,80 +3458,17 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3458
3458
  project_team_path = os.path.abspath(PROJECT_NPC_TEAM_PATH)
3459
3459
  global_team_path = os.path.expanduser(DEFAULT_NPC_TEAM_PATH)
3460
3460
 
3461
- team_dir = None
3462
- default_forenpc_name = None
3463
- global_team_path = os.path.expanduser(DEFAULT_NPC_TEAM_PATH)
3464
3461
  if not os.path.exists(global_team_path):
3465
- print("Global NPC team directory doesn't exist. Initializing...")
3466
3462
  initialize_base_npcs_if_needed(db_path)
3467
3463
  if os.path.exists(project_team_path):
3468
3464
  team_dir = project_team_path
3469
3465
  default_forenpc_name = "forenpc"
3470
3466
  else:
3471
- if not os.path.exists('.npcsh_global'):
3472
- try:
3473
- resp = input(f"No npc_team found in {os.getcwd()}. Create a new team here? [Y/n]: ").strip().lower()
3474
- except (KeyboardInterrupt, EOFError):
3475
- print("\nAborted.")
3476
- sys.exit(0)
3477
- if resp in ("", "y", "yes"):
3478
- team_dir = project_team_path
3479
- os.makedirs(team_dir, exist_ok=True)
3480
- default_forenpc_name = "forenpc"
3481
- try:
3482
- forenpc_directive = input(
3483
- f"Enter a primary directive for {default_forenpc_name} (default: 'You are the forenpc of the team...'): "
3484
- ).strip() or "You are the forenpc of the team, coordinating activities between NPCs on the team, verifying that results from NPCs are high quality and can help to adequately answer user requests."
3485
- forenpc_model = input("Enter a model for your forenpc (default: llama3.2): ").strip() or "llama3.2"
3486
- forenpc_provider = input("Enter a provider for your forenpc (default: ollama): ").strip() or "ollama"
3487
- except (KeyboardInterrupt, EOFError):
3488
- print("\nAborted.")
3489
- sys.exit(0)
3490
-
3491
- with open(os.path.join(team_dir, f"{default_forenpc_name}.npc"), "w") as f:
3492
- yaml.dump({
3493
- "name": default_forenpc_name, "primary_directive": forenpc_directive,
3494
- "model": forenpc_model, "provider": forenpc_provider
3495
- }, f)
3496
-
3497
- ctx_path = os.path.join(team_dir, "team.ctx")
3498
- try:
3499
- folder_context = input("Enter a short description for this project/team (optional): ").strip()
3500
- team_ctx_data = {
3501
- "forenpc": default_forenpc_name,
3502
- "model": forenpc_model,
3503
- "provider": forenpc_provider,
3504
- "context": folder_context if folder_context else None
3505
- }
3506
- use_jinxs = input("Use global jinxs folder (g) or copy to this project (c)? [g/c, default: g]: ").strip().lower()
3507
- except (KeyboardInterrupt, EOFError):
3508
- print("\nAborted.")
3509
- sys.exit(0)
3510
- if use_jinxs == "c":
3511
- global_jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
3512
- if os.path.exists(global_jinxs_dir):
3513
- # Create the 'jinxs' subfolder within the new team's directory
3514
- destination_jinxs_dir = os.path.join(team_dir, "jinxs")
3515
- os.makedirs(destination_jinxs_dir, exist_ok=True)
3516
- shutil.copytree(global_jinxs_dir, destination_jinxs_dir, dirs_exist_ok=True)
3517
- else:
3518
- team_ctx_data["use_global_jinxs"] = True
3519
- with open(ctx_path, "w") as f:
3520
- yaml.dump(team_ctx_data, f)
3521
- else:
3522
- render_markdown('From now on, npcsh will assume you will use the global team when activating from this folder. \n If you change your mind and want to initialize a team, use /init from within npcsh, `npc init` or `rm .npcsh_global` from the current working directory.')
3523
- with open(".npcsh_global", "w") as f:
3524
- pass
3525
- team_dir = global_team_path
3526
- default_forenpc_name = "sibiji"
3527
- else:
3528
- team_dir = global_team_path
3529
- default_forenpc_name = "sibiji"
3530
-
3531
- if team_dir is None:
3467
+ # No project team in this directory - use global team.
3468
+ # To create a project team, use /init from within npcsh or `npc init`.
3532
3469
  team_dir = global_team_path
3533
3470
  default_forenpc_name = "sibiji"
3534
-
3471
+
3535
3472
  if not os.path.exists(team_dir):
3536
3473
  print(f"Creating team directory: {team_dir}")
3537
3474
  os.makedirs(team_dir, exist_ok=True)
@@ -3548,11 +3485,8 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
3548
3485
  forenpc_name = team_ctx.get("forenpc", default_forenpc_name)
3549
3486
  if forenpc_name is None:
3550
3487
  forenpc_name = "sibiji"
3551
-
3552
- print('forenpc_name:', forenpc_name)
3553
3488
 
3554
3489
  forenpc_path = os.path.join(team_dir, f"{forenpc_name}.npc")
3555
- print('forenpc_path:', forenpc_path)
3556
3490
 
3557
3491
  team = Team(team_path=team_dir, db_conn=command_history.engine)
3558
3492
 
@@ -6,7 +6,7 @@ import os
6
6
  import sys
7
7
  import difflib
8
8
  from dataclasses import dataclass, field
9
- from typing import List, Dict, Optional, Tuple
9
+ from typing import List, Dict, Tuple
10
10
  from enum import Enum
11
11
 
12
12
  # Platform-specific imports
@@ -324,8 +324,8 @@ class DiffViewer:
324
324
  start = hunk.start_original - 1 + offset
325
325
 
326
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('+')]
327
+ removals = [ln[1:] for ln in hunk.lines if ln.startswith('-')]
328
+ additions = [ln[1:] for ln in hunk.lines if ln.startswith('+')]
329
329
 
330
330
  # Remove old lines
331
331
  del result_lines[start:start + len(removals)]
@@ -0,0 +1,428 @@
1
+ jinx_name: "compress"
2
+ description: "Manages conversation and knowledge context - compress, flush, sleep, dream"
3
+ interactive: true
4
+ inputs:
5
+ - flush: ""
6
+ - sleep: False
7
+ - dream: False
8
+ - ops: ""
9
+ - model: ""
10
+ - provider: ""
11
+ steps:
12
+ - name: "manage_context_and_memory"
13
+ engine: "python"
14
+ code: |
15
+ import os
16
+ import sys
17
+ import traceback
18
+ from npcpy.llm_funcs import breathe
19
+ from npcpy.memory.command_history import CommandHistory, load_kg_from_db, save_kg_to_db
20
+ from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process
21
+
22
+ # --- Get all inputs from context ---
23
+ flush_n_str = context.get('flush')
24
+ is_sleeping = context.get('sleep')
25
+ is_dreaming = context.get('dream')
26
+ operations_str = context.get('ops')
27
+ llm_model = context.get('model')
28
+ llm_provider = context.get('provider')
29
+ output_messages = context.get('messages', [])
30
+
31
+ # --- Detect if called with explicit flags ---
32
+ is_flushing = flush_n_str is not None and str(flush_n_str).strip() != ''
33
+ has_explicit_args = is_flushing or is_sleeping or is_dreaming
34
+
35
+ # ========== Execution Functions ==========
36
+ def do_compress():
37
+ """Compact conversation context via breathe()"""
38
+ try:
39
+ result = breathe(**context)
40
+ if isinstance(result, dict):
41
+ return result.get('output', 'Context compressed.')
42
+ return "Context compression process initiated."
43
+ except Exception as e:
44
+ traceback.print_exc()
45
+ return "Error during context compression: " + str(e)
46
+
47
+ def do_flush(n):
48
+ """Remove last N messages from context"""
49
+ messages_list = list(output_messages)
50
+ original_len = len(messages_list)
51
+ if messages_list and messages_list[0].get("role") == "system":
52
+ system_message = messages_list.pop(0)
53
+ num_to_remove = min(n, len(messages_list))
54
+ final_messages = [system_message] + messages_list[:-num_to_remove] if num_to_remove < len(messages_list) else [system_message]
55
+ else:
56
+ num_to_remove = min(n, original_len)
57
+ final_messages = messages_list[:-num_to_remove] if num_to_remove < original_len else []
58
+ removed_count = original_len - len(final_messages)
59
+ context['messages'] = final_messages
60
+ return "Flushed " + str(removed_count) + " message(s). Context is now " + str(len(final_messages)) + " messages."
61
+
62
+ def do_sleep(model, provider, ops_str, dream=False):
63
+ """Evolve knowledge graph via sleep/dream"""
64
+ current_npc = context.get('npc')
65
+ current_team = context.get('team')
66
+ operations_config = [op.strip() for op in ops_str.split(',')] if ops_str else None
67
+ if not model and current_npc: model = current_npc.model
68
+ if not provider and current_npc: provider = current_npc.provider
69
+ if not model: model = state.chat_model if state else "llama3.2"
70
+ if not provider: provider = state.chat_provider if state else "ollama"
71
+ team_name = current_team.name if current_team else "__none__"
72
+ npc_name = current_npc.name if current_npc else "__none__"
73
+ current_path = os.getcwd()
74
+ scope_str = "Team: '" + team_name + "', NPC: '" + npc_name + "', Path: '" + current_path + "'"
75
+ command_history = None
76
+ try:
77
+ db_path = os.getenv("NPCSH_DB_PATH", os.path.expanduser("~/npcsh_history.db"))
78
+ command_history = CommandHistory(db_path)
79
+ engine = command_history.engine
80
+ current_kg = load_kg_from_db(engine, team_name, npc_name, current_path)
81
+ if not current_kg or not current_kg.get('facts'):
82
+ return "Knowledge graph for the current scope is empty. Nothing to process.\n- Scope: " + scope_str
83
+ original_facts = len(current_kg.get('facts', []))
84
+ original_concepts = len(current_kg.get('concepts', []))
85
+ evolved_kg, _ = kg_sleep_process(existing_kg=current_kg, model=model, provider=provider, npc=current_npc, operations_config=operations_config)
86
+ process_type = "Sleep"
87
+ if dream:
88
+ evolved_kg, _ = kg_dream_process(existing_kg=evolved_kg, model=model, provider=provider, npc=current_npc)
89
+ process_type += " & Dream"
90
+ save_kg_to_db(engine, evolved_kg, team_name, npc_name, current_path)
91
+ new_facts = len(evolved_kg.get('facts', []))
92
+ new_concepts = len(evolved_kg.get('concepts', []))
93
+ return (process_type + " process complete.\n"
94
+ "- Facts: " + str(original_facts) + " -> " + str(new_facts) + " (" + str(new_facts - original_facts) + ")\n"
95
+ "- Concepts: " + str(original_concepts) + " -> " + str(new_concepts) + " (" + str(new_concepts - original_concepts) + ")")
96
+ except Exception as e:
97
+ traceback.print_exc()
98
+ return "Error during KG evolution: " + str(e)
99
+ finally:
100
+ if command_history: command_history.close()
101
+
102
+ # ========== Direct execution (flags provided) ==========
103
+ if has_explicit_args:
104
+ if is_sleeping and is_flushing:
105
+ context['output'] = "Error: --sleep and --flush are mutually exclusive."
106
+ context['messages'] = output_messages
107
+ elif is_sleeping:
108
+ context['output'] = do_sleep(llm_model, llm_provider, operations_str, dream=bool(is_dreaming))
109
+ context['messages'] = output_messages
110
+ elif is_flushing:
111
+ try:
112
+ n = int(flush_n_str)
113
+ if n <= 0:
114
+ context['output'] = "Error: Number of messages to flush must be positive."
115
+ else:
116
+ context['output'] = do_flush(n)
117
+ except ValueError:
118
+ context['output'] = "Error: Invalid number '" + str(flush_n_str) + "'."
119
+
120
+ # ========== Interactive TUI (no flags) ==========
121
+ elif sys.stdin.isatty():
122
+ import tty
123
+ import termios
124
+ import select as _sel
125
+
126
+ class CompressState:
127
+ def __init__(self):
128
+ self.actions = [
129
+ {'name': 'Compress', 'desc': 'Compact conversation context (breathe)', 'key': 'compress'},
130
+ {'name': 'Flush', 'desc': 'Remove last N messages from context', 'key': 'flush'},
131
+ {'name': 'Sleep', 'desc': 'Evolve knowledge graph', 'key': 'sleep'},
132
+ {'name': 'Sleep + Dream', 'desc': 'Evolve KG with creative synthesis', 'key': 'dream'},
133
+ ]
134
+ self.sel = 0
135
+ self.mode = 'menu' # menu, params, editing, running
136
+ # Params for each action
137
+ self.flush_n = "5"
138
+ self.sleep_model = ""
139
+ self.sleep_provider = ""
140
+ self.sleep_ops = ""
141
+ self.status = "Select an action"
142
+ self.msg_count = len(output_messages)
143
+ # Param editing
144
+ self.param_sel = 0
145
+ self.edit_buf = ""
146
+ self.edit_cursor = 0
147
+
148
+ def get_params(self):
149
+ """Return list of (label, value) for current action"""
150
+ key = self.actions[self.sel]['key']
151
+ if key == 'compress':
152
+ return [('Messages in context', str(self.msg_count), False)]
153
+ elif key == 'flush':
154
+ return [
155
+ ('Messages to flush', self.flush_n, True),
156
+ ('Messages in context', str(self.msg_count), False),
157
+ ]
158
+ elif key == 'sleep':
159
+ return [
160
+ ('Model', self.sleep_model or '(default)', True),
161
+ ('Provider', self.sleep_provider or '(default)', True),
162
+ ('Operations', self.sleep_ops or '(all)', True),
163
+ ]
164
+ elif key == 'dream':
165
+ return [
166
+ ('Model', self.sleep_model or '(default)', True),
167
+ ('Provider', self.sleep_provider or '(default)', True),
168
+ ('Operations', self.sleep_ops or '(all)', True),
169
+ ]
170
+ return []
171
+
172
+ def set_param(self, idx, val):
173
+ key = self.actions[self.sel]['key']
174
+ if key == 'flush':
175
+ if idx == 0: self.flush_n = val
176
+ elif key in ('sleep', 'dream'):
177
+ if idx == 0: self.sleep_model = val
178
+ elif idx == 1: self.sleep_provider = val
179
+ elif idx == 2: self.sleep_ops = val
180
+
181
+ st = CompressState()
182
+
183
+ def get_size():
184
+ try:
185
+ s = os.get_terminal_size()
186
+ return s.columns, s.lines
187
+ except:
188
+ return 80, 24
189
+
190
+ def render():
191
+ width, height = get_size()
192
+ out = []
193
+ out.append("\033[H")
194
+
195
+ # Header
196
+ header = " COMPRESS - Context & Memory Manager "
197
+ out.append("\033[1;1H\033[7;1m" + header.ljust(width) + "\033[0m")
198
+
199
+ if st.mode == 'menu':
200
+ out.append("\033[3;1H\033[36;1m Actions \033[90m" + ("-" * (width - 11)) + "\033[0m")
201
+ for i, act in enumerate(st.actions):
202
+ row = 4 + i
203
+ out.append("\033[" + str(row) + ";1H\033[K")
204
+ line = " " + act['name'].ljust(18) + "\033[90m" + act['desc'][:width-24] + "\033[0m"
205
+ if i == st.sel:
206
+ out.append("\033[7m>" + line + "\033[0m")
207
+ else:
208
+ out.append(" " + line)
209
+
210
+ # Show params preview below
211
+ params = st.get_params()
212
+ param_start = 4 + len(st.actions) + 1
213
+ out.append("\033[" + str(param_start) + ";1H\033[33;1m Parameters \033[90m" + ("-" * (width - 14)) + "\033[0m")
214
+ for j, (label, val, editable) in enumerate(params):
215
+ row = param_start + 1 + j
216
+ out.append("\033[" + str(row) + ";1H\033[K")
217
+ marker = "[e]" if editable else " "
218
+ out.append(" " + label.ljust(22) + val[:width-30] + " \033[90m" + marker + "\033[0m")
219
+
220
+ # Clear remaining lines
221
+ clear_start = param_start + 1 + len(params)
222
+ for r in range(clear_start, height - 2):
223
+ out.append("\033[" + str(r) + ";1H\033[K")
224
+
225
+ elif st.mode == 'params':
226
+ params = st.get_params()
227
+ act = st.actions[st.sel]
228
+ out.append("\033[3;1H\033[36;1m " + act['name'] + " Parameters \033[90m" + ("-" * (width - len(act['name']) - 16)) + "\033[0m")
229
+ for j, (label, val, editable) in enumerate(params):
230
+ row = 4 + j
231
+ out.append("\033[" + str(row) + ";1H\033[K")
232
+ if st.mode == 'editing' and j == st.param_sel:
233
+ line = " " + label.ljust(22) + "\033[7m " + st.edit_buf + " \033[0m"
234
+ else:
235
+ marker = " [e]" if editable else ""
236
+ line = " " + label.ljust(22) + val[:width-30] + "\033[90m" + marker + "\033[0m"
237
+ if j == st.param_sel:
238
+ out.append("\033[7m>" + line + "\033[0m")
239
+ else:
240
+ out.append(" " + line)
241
+
242
+ clear_start = 4 + len(params)
243
+ for r in range(clear_start, height - 2):
244
+ out.append("\033[" + str(r) + ";1H\033[K")
245
+
246
+ elif st.mode == 'editing':
247
+ # Same as params but with edit field
248
+ params = st.get_params()
249
+ act = st.actions[st.sel]
250
+ out.append("\033[3;1H\033[36;1m " + act['name'] + " Parameters \033[90m" + ("-" * (width - len(act['name']) - 16)) + "\033[0m")
251
+ for j, (label, val, editable) in enumerate(params):
252
+ row = 4 + j
253
+ out.append("\033[" + str(row) + ";1H\033[K")
254
+ if j == st.param_sel:
255
+ line = " " + label.ljust(22) + "\033[7m " + st.edit_buf + " \033[0m"
256
+ out.append(">" + line)
257
+ else:
258
+ marker = " [e]" if editable else ""
259
+ line = " " + label.ljust(22) + val[:width-30] + "\033[90m" + marker + "\033[0m"
260
+ out.append(" " + line)
261
+
262
+ clear_start = 4 + len(params)
263
+ for r in range(clear_start, height - 2):
264
+ out.append("\033[" + str(r) + ";1H\033[K")
265
+
266
+ elif st.mode == 'running':
267
+ out.append("\033[3;1H\033[K")
268
+ out.append("\033[4;1H\033[K Running " + st.actions[st.sel]['name'] + "...")
269
+ for r in range(5, height - 2):
270
+ out.append("\033[" + str(r) + ";1H\033[K")
271
+
272
+ # Status + footer
273
+ out.append("\033[" + str(height-2) + ";1H\033[K\033[90m" + ("-" * width) + "\033[0m")
274
+ out.append("\033[" + str(height-1) + ";1H\033[K " + st.status[:width-2])
275
+
276
+ if st.mode == 'menu':
277
+ footer = " j/k:Nav Enter:Configure G:Run Now q:Quit "
278
+ elif st.mode == 'params':
279
+ footer = " j/k:Nav e:Edit Enter:Execute b:Back q:Quit "
280
+ elif st.mode == 'editing':
281
+ footer = " Type value Enter:Confirm Esc:Cancel "
282
+ else:
283
+ footer = " Running... "
284
+ out.append("\033[" + str(height) + ";1H\033[K\033[7m" + footer.ljust(width) + "\033[0m")
285
+
286
+ sys.stdout.write(''.join(out))
287
+ sys.stdout.flush()
288
+
289
+ def execute_action():
290
+ """Run the selected action and return result text"""
291
+ key = st.actions[st.sel]['key']
292
+ st.mode = 'running'
293
+ render()
294
+
295
+ if key == 'compress':
296
+ return do_compress()
297
+ elif key == 'flush':
298
+ try:
299
+ n = int(st.flush_n)
300
+ if n <= 0:
301
+ return "Error: Number must be positive."
302
+ return do_flush(n)
303
+ except ValueError:
304
+ return "Error: Invalid number '" + st.flush_n + "'."
305
+ elif key == 'sleep':
306
+ return do_sleep(st.sleep_model or None, st.sleep_provider or None, st.sleep_ops or None, dream=False)
307
+ elif key == 'dream':
308
+ return do_sleep(st.sleep_model or None, st.sleep_provider or None, st.sleep_ops or None, dream=True)
309
+ return "Unknown action."
310
+
311
+ fd = sys.stdin.fileno()
312
+ old_settings = termios.tcgetattr(fd)
313
+ result_text = None
314
+
315
+ try:
316
+ tty.setcbreak(fd)
317
+ sys.stdout.write('\033[?25l')
318
+ sys.stdout.write('\033[2J')
319
+ render()
320
+
321
+ running = True
322
+ while running:
323
+ c = os.read(fd, 1).decode('latin-1')
324
+
325
+ if st.mode == 'editing':
326
+ if c == '\x1b':
327
+ if _sel.select([fd], [], [], 0.05)[0]:
328
+ os.read(fd, 2)
329
+ st.mode = 'params'
330
+ elif c in ('\r', '\n'):
331
+ st.set_param(st.param_sel, st.edit_buf)
332
+ st.mode = 'params'
333
+ st.status = "Parameter updated."
334
+ elif c == '\x7f' or c == '\x08':
335
+ if st.edit_cursor > 0:
336
+ st.edit_buf = st.edit_buf[:st.edit_cursor-1] + st.edit_buf[st.edit_cursor:]
337
+ st.edit_cursor -= 1
338
+ elif c >= ' ' and c <= '~':
339
+ st.edit_buf = st.edit_buf[:st.edit_cursor] + c + st.edit_buf[st.edit_cursor:]
340
+ st.edit_cursor += 1
341
+ render()
342
+ continue
343
+
344
+ if c == '\x1b':
345
+ if _sel.select([fd], [], [], 0.05)[0]:
346
+ c2 = os.read(fd, 1).decode('latin-1')
347
+ if c2 == '[':
348
+ c3 = os.read(fd, 1).decode('latin-1')
349
+ if c3 == 'A': # Up
350
+ if st.mode == 'menu':
351
+ st.sel = max(0, st.sel - 1)
352
+ elif st.mode == 'params':
353
+ st.param_sel = max(0, st.param_sel - 1)
354
+ elif c3 == 'B': # Down
355
+ if st.mode == 'menu':
356
+ st.sel = min(len(st.actions) - 1, st.sel + 1)
357
+ elif st.mode == 'params':
358
+ params = st.get_params()
359
+ st.param_sel = min(len(params) - 1, st.param_sel + 1)
360
+ else:
361
+ if st.mode == 'params':
362
+ st.mode = 'menu'
363
+ st.status = "Select an action"
364
+ else:
365
+ result_text = "Cancelled."
366
+ running = False
367
+ render()
368
+ continue
369
+
370
+ if c == 'q' or c == '\x03':
371
+ result_text = "Cancelled."
372
+ running = False
373
+ elif st.mode == 'menu':
374
+ if c == 'j':
375
+ st.sel = min(len(st.actions) - 1, st.sel + 1)
376
+ elif c == 'k':
377
+ st.sel = max(0, st.sel - 1)
378
+ elif c in ('\r', '\n'):
379
+ params = st.get_params()
380
+ editable = [p for p in params if p[2]]
381
+ if editable:
382
+ st.mode = 'params'
383
+ st.param_sel = 0
384
+ st.status = "Configure parameters, then Enter to execute"
385
+ else:
386
+ result_text = execute_action()
387
+ running = False
388
+ elif c == 'G':
389
+ result_text = execute_action()
390
+ running = False
391
+ elif st.mode == 'params':
392
+ if c == 'j':
393
+ params = st.get_params()
394
+ st.param_sel = min(len(params) - 1, st.param_sel + 1)
395
+ elif c == 'k':
396
+ st.param_sel = max(0, st.param_sel - 1)
397
+ elif c == 'e':
398
+ params = st.get_params()
399
+ if st.param_sel < len(params) and params[st.param_sel][2]:
400
+ st.mode = 'editing'
401
+ val = params[st.param_sel][1]
402
+ st.edit_buf = "" if val.startswith('(') else val
403
+ st.edit_cursor = len(st.edit_buf)
404
+ elif c == 'b':
405
+ st.mode = 'menu'
406
+ st.status = "Select an action"
407
+ elif c in ('\r', '\n'):
408
+ result_text = execute_action()
409
+ running = False
410
+
411
+ render()
412
+
413
+ finally:
414
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
415
+ sys.stdout.write('\033[?25h')
416
+ sys.stdout.write('\033[2J\033[H')
417
+ sys.stdout.flush()
418
+
419
+ context['output'] = result_text or "Cancelled."
420
+ if 'messages' not in context:
421
+ context['messages'] = output_messages
422
+
423
+ # ========== Non-interactive fallback ==========
424
+ else:
425
+ result = do_compress()
426
+ context['output'] = result
427
+ if 'messages' not in context:
428
+ context['messages'] = output_messages
@@ -1,5 +1,6 @@
1
1
  jinx_name: db_search
2
2
  description: Search conversation history database with interactive TUI
3
+ interactive: true
3
4
  inputs:
4
5
  - query: ""
5
6
  - path: ""
@@ -179,7 +180,7 @@ steps:
179
180
  header = f" DB SEARCH ({len(display_rows)} results): '{query}' [sort:{sort_mode}({sort_ind}) filter:{role_filter}] "
180
181
  else:
181
182
  header = f" PREVIEW: {display_rows[selected]['conversation_id'][:16]} "
182
- sys.stdout.write(f'\033[44;37;1m{header.ljust(width)}\033[0m\n')
183
+ sys.stdout.write(f'\033[7;1m{header.ljust(width)}\033[0m\n')
183
184
 
184
185
  # Column headers
185
186
  if mode == 'list':
@@ -225,7 +226,7 @@ steps:
225
226
  if len(path) > 40:
226
227
  path = '...' + path[-37:]
227
228
  sys.stdout.write(f'\033[{height-1};1H\033[K {cid} @ {path}'.ljust(width))
228
- sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Nav 1/2/3:Sort f:Filter p:Preview r:Reattach q:Quit [{selected+1}/{len(display_rows)}] \033[0m')
229
+ sys.stdout.write(f'\033[{height};1H\033[K\033[7m j/k:Nav 1/2/3:Sort f:Filter p:Preview r:Reattach q:Quit [{selected+1}/{len(display_rows)}] \033[0m')
229
230
 
230
231
  else: # preview mode
231
232
  sel = display_rows[selected]
@@ -243,16 +244,26 @@ steps:
243
244
  npc_name = sel.get('npc') or 'default'
244
245
  ts = format_ts(sel.get('timestamp'))
245
246
  sys.stdout.write(f'\033[{height-1};1H\033[K {role}/{npc_name} - {ts} [{preview_scroll+1}/{len(lines)} lines]')
246
- sys.stdout.write(f'\033[{height};1H\033[K\033[44;37m j/k:Scroll b:Back r:Reattach q:Quit \033[0m')
247
+ sys.stdout.write(f'\033[{height};1H\033[K\033[7m j/k:Scroll b:Back r:Reattach q:Quit \033[0m')
247
248
 
248
249
  sys.stdout.flush()
249
250
 
250
- c = sys.stdin.read(1)
251
+ c = os.read(fd, 1).decode('latin-1')
251
252
 
252
253
  if c == '\x1b':
253
- c2 = sys.stdin.read(1)
254
+ import select as _sel
255
+ if _sel.select([fd], [], [], 0.05)[0]:
256
+ c2 = os.read(fd, 1).decode('latin-1')
257
+ else:
258
+ if mode == 'preview':
259
+ mode = 'list'
260
+ sys.stdout.write('\033[2J\033[H')
261
+ else:
262
+ context['output'] = "Cancelled."
263
+ break
264
+ continue
254
265
  if c2 == '[':
255
- c3 = sys.stdin.read(1)
266
+ c3 = os.read(fd, 1).decode('latin-1')
256
267
  if c3 == 'A': # Up
257
268
  if mode == 'list' and selected > 0:
258
269
  selected -= 1