npcsh 1.1.18__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 (139) hide show
  1. npcsh/_state.py +8 -0
  2. npcsh/benchmark/npcsh_agent.py +47 -16
  3. npcsh/config.py +1 -0
  4. npcsh/diff_viewer.py +452 -0
  5. npcsh/npc_team/jinxs/bin/config_tui.jinx +299 -0
  6. npcsh/npc_team/jinxs/bin/memories.jinx +316 -0
  7. npcsh/npc_team/jinxs/bin/setup.jinx +240 -0
  8. npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
  9. npcsh/npc_team/jinxs/bin/team_tui.jinx +327 -0
  10. npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
  11. npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
  12. npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
  13. npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
  14. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
  15. npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
  16. npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
  17. npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
  18. npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
  19. npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
  20. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
  21. npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
  22. npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
  23. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
  24. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
  25. npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
  26. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
  27. npcsh/npc_team/jinxs/modes/guac.jinx +0 -2
  28. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
  29. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
  30. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
  31. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +299 -0
  32. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
  33. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
  34. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.jinx +0 -2
  35. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
  36. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
  37. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
  38. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
  39. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
  40. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
  41. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
  42. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/send_message.jinx +1 -1
  43. npcsh-1.1.19.data/data/npcsh/npc_team/setup.jinx +240 -0
  44. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
  45. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
  46. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
  47. npcsh-1.1.19.data/data/npcsh/npc_team/sync.jinx +223 -0
  48. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +327 -0
  49. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
  50. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
  51. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/METADATA +21 -14
  52. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/RECORD +138 -129
  53. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -0
  54. npcsh-1.1.18.data/data/npcsh/npc_team/sync.jinx +0 -230
  55. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
  56. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  57. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
  58. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  59. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  60. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  61. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  62. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
  63. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
  64. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
  65. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  66. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  67. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
  68. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
  69. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
  70. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.jinx +0 -0
  71. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
  72. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
  73. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
  74. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  75. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  76. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  77. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  78. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
  79. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
  80. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
  81. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
  82. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
  83. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  84. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
  85. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
  86. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  87. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  88. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  89. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kg_search.jinx +0 -0
  90. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  91. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  92. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
  93. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/mem_search.jinx +0 -0
  94. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  95. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  96. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
  97. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  98. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
  99. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paper_search.jinx +0 -0
  100. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
  101. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.jinx +0 -0
  102. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
  103. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
  104. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  105. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  106. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/pti.jinx +0 -0
  107. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
  108. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  109. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
  110. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
  111. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  112. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
  113. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/semantic_scholar.jinx +0 -0
  114. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
  115. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
  116. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
  117. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
  118. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  119. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
  120. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  121. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.jinx +0 -0
  122. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
  123. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sql.jinx +0 -0
  124. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
  125. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
  126. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  127. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  128. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  129. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
  130. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  131. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  132. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
  133. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wander.jinx +0 -0
  134. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  135. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +0 -0
  136. {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
  137. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/WHEEL +0 -0
  138. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
  139. {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,299 @@
1
+ jinx_name: config_tui
2
+ description: Interactive TUI editor for npcsh configuration (~/.npcshrc)
3
+ inputs: []
4
+ steps:
5
+ - name: config_editor
6
+ engine: python
7
+ code: |
8
+ import os
9
+ import sys
10
+ import tty
11
+ import termios
12
+ import select
13
+ from pathlib import Path
14
+
15
+ if not sys.stdin.isatty():
16
+ context['output'] = "Config TUI requires an interactive terminal."
17
+ return
18
+
19
+ # ========== Config Items ==========
20
+ CONFIG_ITEMS = [
21
+ {'key': 'NPCSH_CHAT_MODEL', 'label': 'Chat Model', 'type': 'text', 'shortcut': 'model'},
22
+ {'key': 'NPCSH_CHAT_PROVIDER', 'label': 'Chat Provider', 'type': 'text', 'shortcut': 'provider'},
23
+ {'key': 'NPCSH_VISION_MODEL', 'label': 'Vision Model', 'type': 'text'},
24
+ {'key': 'NPCSH_VISION_PROVIDER', 'label': 'Vision Provider', 'type': 'text'},
25
+ {'key': 'NPCSH_EMBEDDING_MODEL', 'label': 'Embedding Model', 'type': 'text'},
26
+ {'key': 'NPCSH_EMBEDDING_PROVIDER', 'label': 'Embedding Provider', 'type': 'text'},
27
+ {'key': 'NPCSH_REASONING_MODEL', 'label': 'Reasoning Model', 'type': 'text'},
28
+ {'key': 'NPCSH_REASONING_PROVIDER', 'label': 'Reasoning Provider', 'type': 'text'},
29
+ {'key': 'NPCSH_DEFAULT_MODE', 'label': 'Default Mode', 'type': 'choice', 'choices': ['agent', 'chat', 'code']},
30
+ {'key': 'NPCSH_STREAM_OUTPUT', 'label': 'Stream Output', 'type': 'toggle'},
31
+ {'key': 'NPCSH_BUILD_KG', 'label': 'Build Knowledge Graph', 'type': 'toggle'},
32
+ {'key': 'NPCSH_SEARCH_PROVIDER', 'label': 'Search Provider', 'type': 'choice', 'choices': ['duckduckgo', 'google', 'bing']},
33
+ ]
34
+
35
+ # ========== State ==========
36
+ class ConfigState:
37
+ def __init__(self):
38
+ self.selected_idx = 0
39
+ self.scroll_offset = 0
40
+ self.editing = False
41
+ self.edit_buffer = ""
42
+ self.edit_cursor = 0
43
+ self.values = {}
44
+ self.modified = set()
45
+ self.status = ""
46
+
47
+ state = ConfigState()
48
+
49
+ # ========== Helpers ==========
50
+ def get_size():
51
+ try:
52
+ s = os.get_terminal_size()
53
+ return s.columns, s.lines
54
+ except:
55
+ return 80, 24
56
+
57
+ def load_values():
58
+ """Load current values from environment and npcshrc."""
59
+ for item in CONFIG_ITEMS:
60
+ key = item['key']
61
+ # First try environment
62
+ value = os.environ.get(key, '')
63
+ if not value:
64
+ # Try reading from npcshrc
65
+ npcshrc = Path.home() / '.npcshrc'
66
+ if npcshrc.exists():
67
+ with open(npcshrc) as f:
68
+ for line in f:
69
+ if line.strip().startswith(f'export {key}='):
70
+ value = line.split('=', 1)[1].strip().strip('"').strip("'")
71
+ break
72
+ state.values[key] = value
73
+
74
+ def save_values():
75
+ """Save modified values to ~/.npcshrc."""
76
+ from npcsh.config import set_npcsh_config_value
77
+ for key in state.modified:
78
+ set_npcsh_config_value(key, state.values[key])
79
+ state.modified.clear()
80
+ state.status = "Saved!"
81
+
82
+ def format_value(item, value):
83
+ """Format value for display."""
84
+ if item['type'] == 'toggle':
85
+ return '\033[32mON\033[0m' if value in ('1', 'true', 'True', True) else '\033[31mOFF\033[0m'
86
+ elif not value:
87
+ return '\033[90m(not set)\033[0m'
88
+ return value
89
+
90
+ # ========== Rendering ==========
91
+ def render_screen():
92
+ width, height = get_size()
93
+ out = []
94
+ out.append("\033[2J\033[H")
95
+
96
+ # Header
97
+ header = " NPCSH Configuration "
98
+ out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
99
+ out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;1m{header}\033[0m")
100
+
101
+ # Config items
102
+ visible_height = height - 6
103
+ visible = CONFIG_ITEMS[state.scroll_offset:state.scroll_offset + visible_height]
104
+
105
+ label_width = max(len(item['label']) for item in CONFIG_ITEMS) + 2
106
+ value_width = width - label_width - 10
107
+
108
+ for i, item in enumerate(visible):
109
+ row = 3 + i
110
+ idx = i + state.scroll_offset
111
+ key = item['key']
112
+ value = state.values.get(key, '')
113
+ display_value = format_value(item, value)
114
+
115
+ # Indicator for modified
116
+ mod_indicator = '*' if key in state.modified else ' '
117
+
118
+ if idx == state.selected_idx:
119
+ if state.editing:
120
+ # Show edit mode
121
+ out.append(f"\033[{row};2H\033[47;30m{item['label']:<{label_width}}\033[0m")
122
+ # Edit buffer with cursor
123
+ cursor_pos = min(state.edit_cursor, len(state.edit_buffer))
124
+ before = state.edit_buffer[:cursor_pos]
125
+ after = state.edit_buffer[cursor_pos:]
126
+ out.append(f"\033[{row};{label_width+4}H{before}\033[7m \033[0m{after}")
127
+ else:
128
+ out.append(f"\033[{row};2H\033[47;30m{mod_indicator}{item['label']:<{label_width}} {display_value[:value_width]}\033[0m")
129
+ else:
130
+ out.append(f"\033[{row};2H{mod_indicator}{item['label']:<{label_width}} {display_value[:value_width]}")
131
+
132
+ # Type indicator
133
+ type_hint = {'text': '[e]', 'toggle': '[t]', 'choice': '[c]'}.get(item['type'], '')
134
+ out.append(f"\033[{row};{width-4}H\033[90m{type_hint}\033[0m")
135
+
136
+ # Status line
137
+ if state.status:
138
+ out.append(f"\033[{height-2};2H\033[33m{state.status}\033[0m")
139
+
140
+ # Footer
141
+ if state.editing:
142
+ footer = "[Enter] Save [Esc] Cancel"
143
+ else:
144
+ footer = "[j/k] Navigate [e] Edit [t] Toggle [s] Save All [q] Quit"
145
+ out.append(f"\033[{height};1H\033[90m{footer[:width]}\033[0m")
146
+
147
+ sys.stdout.write(''.join(out))
148
+ sys.stdout.flush()
149
+
150
+ # ========== Input Handling ==========
151
+ def handle_input(c):
152
+ if state.editing:
153
+ return handle_edit_input(c)
154
+
155
+ if c == 'q':
156
+ if state.modified:
157
+ state.status = "Unsaved changes! Press 's' to save or 'q' again to discard."
158
+ return True
159
+ return False
160
+
161
+ if c == '\x1b': # Escape sequence
162
+ if select.select([sys.stdin], [], [], 0.05)[0]:
163
+ c2 = sys.stdin.read(1)
164
+ if c2 == '[':
165
+ c3 = sys.stdin.read(1)
166
+ if c3 == 'A': # Up
167
+ move_up()
168
+ elif c3 == 'B': # Down
169
+ move_down()
170
+ return True
171
+
172
+ if c == 'k':
173
+ move_up()
174
+ elif c == 'j':
175
+ move_down()
176
+ elif c == 'e' or c == '\r' or c == '\n':
177
+ start_edit()
178
+ elif c == 't':
179
+ toggle_value()
180
+ elif c == 'c':
181
+ cycle_choice()
182
+ elif c == 's':
183
+ save_values()
184
+
185
+ return True
186
+
187
+ def handle_edit_input(c):
188
+ if c == '\x1b': # Escape - cancel edit
189
+ state.editing = False
190
+ state.edit_buffer = ""
191
+ state.status = "Edit cancelled"
192
+ return True
193
+
194
+ if c == '\r' or c == '\n': # Enter - save edit
195
+ key = CONFIG_ITEMS[state.selected_idx]['key']
196
+ state.values[key] = state.edit_buffer
197
+ state.modified.add(key)
198
+ state.editing = False
199
+ state.edit_buffer = ""
200
+ state.status = f"Changed {key}"
201
+ return True
202
+
203
+ if c == '\x7f' or c == '\x08': # Backspace
204
+ if state.edit_cursor > 0:
205
+ state.edit_buffer = state.edit_buffer[:state.edit_cursor-1] + state.edit_buffer[state.edit_cursor:]
206
+ state.edit_cursor -= 1
207
+ return True
208
+
209
+ if c >= ' ' and c <= '~': # Printable
210
+ state.edit_buffer = state.edit_buffer[:state.edit_cursor] + c + state.edit_buffer[state.edit_cursor:]
211
+ state.edit_cursor += 1
212
+ return True
213
+
214
+ return True
215
+
216
+ def move_up():
217
+ state.selected_idx = max(0, state.selected_idx - 1)
218
+ if state.selected_idx < state.scroll_offset:
219
+ state.scroll_offset = state.selected_idx
220
+ state.status = ""
221
+
222
+ def move_down():
223
+ _, height = get_size()
224
+ visible_height = height - 6
225
+ state.selected_idx = min(len(CONFIG_ITEMS) - 1, state.selected_idx + 1)
226
+ if state.selected_idx >= state.scroll_offset + visible_height:
227
+ state.scroll_offset = state.selected_idx - visible_height + 1
228
+ state.status = ""
229
+
230
+ def start_edit():
231
+ item = CONFIG_ITEMS[state.selected_idx]
232
+ if item['type'] == 'toggle':
233
+ toggle_value()
234
+ elif item['type'] == 'choice':
235
+ cycle_choice()
236
+ else:
237
+ key = item['key']
238
+ state.edit_buffer = state.values.get(key, '')
239
+ state.edit_cursor = len(state.edit_buffer)
240
+ state.editing = True
241
+ state.status = "Editing... Enter to save, Esc to cancel"
242
+
243
+ def toggle_value():
244
+ item = CONFIG_ITEMS[state.selected_idx]
245
+ if item['type'] != 'toggle':
246
+ return
247
+ key = item['key']
248
+ current = state.values.get(key, '0')
249
+ new_value = '0' if current in ('1', 'true', 'True') else '1'
250
+ state.values[key] = new_value
251
+ state.modified.add(key)
252
+ state.status = f"Toggled {item['label']}"
253
+
254
+ def cycle_choice():
255
+ item = CONFIG_ITEMS[state.selected_idx]
256
+ if item['type'] != 'choice':
257
+ return
258
+ key = item['key']
259
+ choices = item.get('choices', [])
260
+ if not choices:
261
+ return
262
+ current = state.values.get(key, '')
263
+ try:
264
+ idx = choices.index(current)
265
+ next_idx = (idx + 1) % len(choices)
266
+ except ValueError:
267
+ next_idx = 0
268
+ state.values[key] = choices[next_idx]
269
+ state.modified.add(key)
270
+ state.status = f"Changed to {choices[next_idx]}"
271
+
272
+ # ========== Main Loop ==========
273
+ load_values()
274
+
275
+ fd = sys.stdin.fileno()
276
+ old_settings = termios.tcgetattr(fd)
277
+
278
+ try:
279
+ tty.setcbreak(fd)
280
+ sys.stdout.write('\033[?25l') # Hide cursor
281
+
282
+ render_screen()
283
+
284
+ while True:
285
+ c = sys.stdin.read(1)
286
+ if not handle_input(c):
287
+ break
288
+ render_screen()
289
+
290
+ finally:
291
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
292
+ sys.stdout.write('\033[?25h') # Show cursor
293
+ sys.stdout.write('\033[2J\033[H') # Clear screen
294
+ sys.stdout.flush()
295
+
296
+ if state.modified:
297
+ context['output'] = f"Exited with unsaved changes: {', '.join(state.modified)}"
298
+ else:
299
+ context['output'] = "Configuration editor closed."
@@ -0,0 +1,316 @@
1
+ jinx_name: memories
2
+ description: Interactive TUI for browsing and managing npcsh memories
3
+ inputs:
4
+ - scope: ""
5
+ steps:
6
+ - name: memory_browser
7
+ engine: python
8
+ code: |
9
+ import os
10
+ import sys
11
+ import tty
12
+ import termios
13
+ import select
14
+ from datetime import datetime
15
+
16
+ if not sys.stdin.isatty():
17
+ context['output'] = "Memory browser requires an interactive terminal."
18
+ return
19
+
20
+ from npcpy.memory.command_history import CommandHistory
21
+ from npcsh.config import NPCSH_DB_PATH
22
+
23
+ db_path = os.path.expanduser(NPCSH_DB_PATH)
24
+ command_history = CommandHistory(db_path)
25
+
26
+ # ========== State ==========
27
+ class MemoryState:
28
+ def __init__(self):
29
+ self.tab = 0 # 0=All, 1=Pending, 2=Approved, 3=Rejected
30
+ self.tabs = ['All', 'Pending', 'Approved', 'Rejected']
31
+ self.memories = []
32
+ self.selected_idx = 0
33
+ self.scroll_offset = 0
34
+ self.preview_mode = False
35
+ self.status = ""
36
+ self.filters = {
37
+ 'npc': None,
38
+ 'team': None,
39
+ }
40
+
41
+ state = MemoryState()
42
+
43
+ # ========== Helpers ==========
44
+ def get_size():
45
+ try:
46
+ s = os.get_terminal_size()
47
+ return s.columns, s.lines
48
+ except:
49
+ return 80, 24
50
+
51
+ def load_memories():
52
+ """Load memories based on current tab filter."""
53
+ state.memories = []
54
+
55
+ try:
56
+ with command_history.engine.connect() as conn:
57
+ status_filter = {
58
+ 0: None, # All
59
+ 1: 'pending',
60
+ 2: 'approved',
61
+ 3: 'rejected'
62
+ }.get(state.tab)
63
+
64
+ query = "SELECT id, created_at, npc_name, team_name, scope, original_memory, final_memory, status FROM memories"
65
+ conditions = []
66
+
67
+ if status_filter:
68
+ conditions.append(f"status = '{status_filter}'")
69
+ if state.filters['npc']:
70
+ conditions.append(f"npc_name = '{state.filters['npc']}'")
71
+ if state.filters['team']:
72
+ conditions.append(f"team_name = '{state.filters['team']}'")
73
+
74
+ if conditions:
75
+ query += " WHERE " + " AND ".join(conditions)
76
+
77
+ query += " ORDER BY created_at DESC LIMIT 100"
78
+
79
+ from sqlalchemy import text
80
+ result = conn.execute(text(query))
81
+ for row in result:
82
+ state.memories.append({
83
+ 'id': row[0],
84
+ 'created_at': row[1],
85
+ 'npc': row[2],
86
+ 'team': row[3],
87
+ 'scope': row[4],
88
+ 'original': row[5],
89
+ 'final': row[6],
90
+ 'status': row[7]
91
+ })
92
+ except Exception as e:
93
+ state.status = f"Error loading memories: {e}"
94
+
95
+ def update_memory_status(memory_id, new_status):
96
+ """Update a memory's status."""
97
+ try:
98
+ command_history.update_memory_status(memory_id, new_status)
99
+ state.status = f"Memory {memory_id} marked as {new_status}"
100
+ load_memories()
101
+ except Exception as e:
102
+ state.status = f"Error: {e}"
103
+
104
+ def format_date(dt_str):
105
+ """Format datetime string for display."""
106
+ if not dt_str:
107
+ return ""
108
+ try:
109
+ if isinstance(dt_str, str):
110
+ dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
111
+ else:
112
+ dt = dt_str
113
+ return dt.strftime('%m-%d %H:%M')
114
+ except:
115
+ return str(dt_str)[:10]
116
+
117
+ # ========== Rendering ==========
118
+ def render_screen():
119
+ width, height = get_size()
120
+ out = []
121
+ out.append("\033[2J\033[H")
122
+
123
+ # Header
124
+ header = " Memory Browser "
125
+ out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
126
+ out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;1m{header}\033[0m")
127
+
128
+ # Tabs
129
+ tab_str = ""
130
+ for i, tab in enumerate(state.tabs):
131
+ count = sum(1 for m in state.memories if state.tab == 0 or True) # Will filter properly
132
+ if i == state.tab:
133
+ tab_str += f"\033[47;30m [{tab}] \033[0m"
134
+ else:
135
+ tab_str += f" [{tab}] "
136
+ out.append(f"\033[2;2H{tab_str}")
137
+
138
+ # Separator
139
+ out.append(f"\033[3;1H\033[90m{'─' * width}\033[0m")
140
+
141
+ if state.preview_mode and state.memories:
142
+ render_preview(out, width, height)
143
+ else:
144
+ render_list(out, width, height)
145
+
146
+ # Status
147
+ if state.status:
148
+ out.append(f"\033[{height-2};2H\033[33m{state.status[:width-4]}\033[0m")
149
+
150
+ # Footer
151
+ if state.preview_mode:
152
+ footer = "[Esc] Back [a] Approve [x] Reject [j/k] Prev/Next"
153
+ else:
154
+ footer = "[Tab] Filter [j/k] Navigate [p] Preview [a] Approve [x] Reject [q] Quit"
155
+ out.append(f"\033[{height};1H\033[90m{footer[:width]}\033[0m")
156
+
157
+ sys.stdout.write(''.join(out))
158
+ sys.stdout.flush()
159
+
160
+ def render_list(out, width, height):
161
+ """Render memory list."""
162
+ visible_height = height - 7
163
+ visible = state.memories[state.scroll_offset:state.scroll_offset + visible_height]
164
+
165
+ if not state.memories:
166
+ out.append(f"\033[5;4H\033[90mNo memories found.\033[0m")
167
+ return
168
+
169
+ row = 4
170
+ for i, mem in enumerate(visible):
171
+ idx = i + state.scroll_offset
172
+
173
+ # Status indicator
174
+ status_icon = {
175
+ 'pending': '\033[33m○\033[0m',
176
+ 'approved': '\033[32m●\033[0m',
177
+ 'rejected': '\033[31m✗\033[0m'
178
+ }.get(mem['status'], '?')
179
+
180
+ # Format line
181
+ date_str = format_date(mem['created_at'])
182
+ npc_str = mem['npc'][:8] if mem['npc'] else '-'
183
+ content = (mem['final'] or mem['original'] or '')[:width-35]
184
+ content = content.replace('\n', ' ')
185
+
186
+ if idx == state.selected_idx:
187
+ out.append(f"\033[{row};2H\033[47;30m{status_icon} {date_str} {npc_str:<8} {content}\033[0m")
188
+ else:
189
+ out.append(f"\033[{row};2H{status_icon} {date_str} \033[90m{npc_str:<8}\033[0m {content}")
190
+
191
+ row += 1
192
+
193
+ # Scroll indicator
194
+ if len(state.memories) > visible_height:
195
+ pct = int((state.scroll_offset / max(1, len(state.memories) - visible_height)) * 100)
196
+ out.append(f"\033[4;{width-6}H\033[90m[{pct}%]\033[0m")
197
+
198
+ def render_preview(out, width, height):
199
+ """Render memory preview."""
200
+ if not state.memories or state.selected_idx >= len(state.memories):
201
+ return
202
+
203
+ mem = state.memories[state.selected_idx]
204
+
205
+ row = 4
206
+ out.append(f"\033[{row};2H\033[1mMemory #{mem['id']}\033[0m")
207
+ row += 1
208
+
209
+ # Metadata
210
+ status_color = {'pending': '33', 'approved': '32', 'rejected': '31'}.get(mem['status'], '0')
211
+ out.append(f"\033[{row};2HStatus: \033[{status_color}m{mem['status']}\033[0m")
212
+ row += 1
213
+ out.append(f"\033[{row};2HDate: {format_date(mem['created_at'])}")
214
+ row += 1
215
+ out.append(f"\033[{row};2HNPC: {mem['npc'] or '-'} Team: {mem['team'] or '-'} Scope: {mem['scope'] or '-'}")
216
+ row += 2
217
+
218
+ # Content
219
+ out.append(f"\033[{row};2H\033[1mContent:\033[0m")
220
+ row += 1
221
+
222
+ content = mem['final'] or mem['original'] or '(empty)'
223
+ content_lines = content.split('\n')
224
+ for line in content_lines[:height-row-3]:
225
+ out.append(f"\033[{row};4H{line[:width-6]}")
226
+ row += 1
227
+
228
+ # ========== Input Handling ==========
229
+ def handle_input(c):
230
+ if c == 'q':
231
+ return False
232
+
233
+ if c == '\x1b': # Escape
234
+ if select.select([sys.stdin], [], [], 0.05)[0]:
235
+ c2 = sys.stdin.read(1)
236
+ if c2 == '[':
237
+ c3 = sys.stdin.read(1)
238
+ if c3 == 'A': # Up
239
+ move_up()
240
+ elif c3 == 'B': # Down
241
+ move_down()
242
+ else:
243
+ if state.preview_mode:
244
+ state.preview_mode = False
245
+ return True
246
+
247
+ if c == '\t': # Tab - cycle tabs
248
+ state.tab = (state.tab + 1) % len(state.tabs)
249
+ state.selected_idx = 0
250
+ state.scroll_offset = 0
251
+ load_memories()
252
+ state.status = ""
253
+
254
+ elif c == 'k':
255
+ move_up()
256
+ elif c == 'j':
257
+ move_down()
258
+ elif c == 'p' or c == '\r' or c == '\n':
259
+ if state.memories:
260
+ state.preview_mode = not state.preview_mode
261
+ elif c == 'a':
262
+ approve_current()
263
+ elif c == 'x':
264
+ reject_current()
265
+
266
+ return True
267
+
268
+ def move_up():
269
+ state.selected_idx = max(0, state.selected_idx - 1)
270
+ if state.selected_idx < state.scroll_offset:
271
+ state.scroll_offset = state.selected_idx
272
+ state.status = ""
273
+
274
+ def move_down():
275
+ _, height = get_size()
276
+ visible_height = height - 7
277
+ state.selected_idx = min(len(state.memories) - 1, state.selected_idx + 1)
278
+ if state.selected_idx >= state.scroll_offset + visible_height:
279
+ state.scroll_offset = state.selected_idx - visible_height + 1
280
+ state.status = ""
281
+
282
+ def approve_current():
283
+ if state.memories and state.selected_idx < len(state.memories):
284
+ mem = state.memories[state.selected_idx]
285
+ update_memory_status(mem['id'], 'approved')
286
+
287
+ def reject_current():
288
+ if state.memories and state.selected_idx < len(state.memories):
289
+ mem = state.memories[state.selected_idx]
290
+ update_memory_status(mem['id'], 'rejected')
291
+
292
+ # ========== Main Loop ==========
293
+ load_memories()
294
+
295
+ fd = sys.stdin.fileno()
296
+ old_settings = termios.tcgetattr(fd)
297
+
298
+ try:
299
+ tty.setcbreak(fd)
300
+ sys.stdout.write('\033[?25l') # Hide cursor
301
+
302
+ render_screen()
303
+
304
+ while True:
305
+ c = sys.stdin.read(1)
306
+ if not handle_input(c):
307
+ break
308
+ render_screen()
309
+
310
+ finally:
311
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
312
+ sys.stdout.write('\033[?25h') # Show cursor
313
+ sys.stdout.write('\033[2J\033[H') # Clear screen
314
+ sys.stdout.flush()
315
+
316
+ context['output'] = "Memory browser closed."