npcsh 1.1.22__py3-none-any.whl → 1.1.23__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 (172) hide show
  1. npcsh/_state.py +272 -120
  2. npcsh/benchmark/npcsh_agent.py +77 -240
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
  4. npcsh/config.py +5 -2
  5. npcsh/npc_team/alicanto.npc +4 -8
  6. npcsh/npc_team/corca.npc +5 -11
  7. npcsh/npc_team/frederic.npc +4 -6
  8. npcsh/npc_team/guac.npc +4 -4
  9. npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
  10. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +1 -1
  11. npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
  12. npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
  13. npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
  14. npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
  15. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
  16. npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
  17. npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
  18. npcsh/npc_team/jinxs/modes/convene.jinx +76 -3
  19. npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
  20. npcsh/npc_team/jinxs/modes/plonk.jinx +76 -14
  21. npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
  22. npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
  23. npcsh/npc_team/jinxs/modes/yap.jinx +504 -30
  24. npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
  25. npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
  26. npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
  27. npcsh/npc_team/kadiefa.npc +4 -5
  28. npcsh/npc_team/npcsh.ctx +16 -0
  29. npcsh/npc_team/plonk.npc +5 -9
  30. npcsh/npc_team/sibiji.npc +13 -5
  31. npcsh/npcsh.py +1 -0
  32. npcsh/routes.py +0 -4
  33. npcsh/yap.py +22 -4
  34. npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
  35. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +4 -8
  36. npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
  37. npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
  38. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -11
  39. npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
  40. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
  41. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/edit_file.jinx +1 -1
  42. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +4 -6
  43. npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
  44. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
  45. npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
  46. npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
  47. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
  48. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +4 -5
  49. npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
  50. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +76 -14
  51. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
  52. npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
  53. npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
  54. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
  55. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +13 -5
  56. npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
  57. npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
  58. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.jinx +504 -30
  59. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/METADATA +168 -7
  60. npcsh-1.1.23.dist-info/RECORD +216 -0
  61. npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
  62. npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
  63. npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
  64. npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
  65. npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
  66. npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
  67. npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
  68. npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
  69. npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
  70. npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
  71. npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
  72. npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
  73. npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
  74. npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
  75. npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
  76. npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
  77. npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
  78. npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
  79. npcsh-1.1.22.data/data/npcsh/npc_team/add_tab.jinx +0 -11
  80. npcsh-1.1.22.data/data/npcsh/npc_team/close_pane.jinx +0 -9
  81. npcsh-1.1.22.data/data/npcsh/npc_team/close_tab.jinx +0 -10
  82. npcsh-1.1.22.data/data/npcsh/npc_team/confirm.jinx +0 -10
  83. npcsh-1.1.22.data/data/npcsh/npc_team/convene.jinx +0 -232
  84. npcsh-1.1.22.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
  85. npcsh-1.1.22.data/data/npcsh/npc_team/help.jinx +0 -52
  86. npcsh-1.1.22.data/data/npcsh/npc_team/init.jinx +0 -41
  87. npcsh-1.1.22.data/data/npcsh/npc_team/list_panes.jinx +0 -8
  88. npcsh-1.1.22.data/data/npcsh/npc_team/navigate.jinx +0 -10
  89. npcsh-1.1.22.data/data/npcsh/npc_team/notify.jinx +0 -10
  90. npcsh-1.1.22.data/data/npcsh/npc_team/npcsh.ctx +0 -18
  91. npcsh-1.1.22.data/data/npcsh/npc_team/open_pane.jinx +0 -13
  92. npcsh-1.1.22.data/data/npcsh/npc_team/read_pane.jinx +0 -9
  93. npcsh-1.1.22.data/data/npcsh/npc_team/roll.jinx +0 -65
  94. npcsh-1.1.22.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
  95. npcsh-1.1.22.data/data/npcsh/npc_team/send_message.jinx +0 -10
  96. npcsh-1.1.22.data/data/npcsh/npc_team/serve.jinx +0 -26
  97. npcsh-1.1.22.data/data/npcsh/npc_team/split_pane.jinx +0 -12
  98. npcsh-1.1.22.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
  99. npcsh-1.1.22.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
  100. npcsh-1.1.22.data/data/npcsh/npc_team/write_file.jinx +0 -11
  101. npcsh-1.1.22.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
  102. npcsh-1.1.22.dist-info/RECORD +0 -240
  103. /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
  104. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
  105. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
  106. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
  107. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
  108. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  109. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  110. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/build.jinx +0 -0
  111. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
  112. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
  113. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  114. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  115. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
  116. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
  117. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.jinx +0 -0
  118. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
  119. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
  120. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
  121. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
  122. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
  123. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
  124. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
  125. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
  126. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  127. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  128. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  129. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +0 -0
  130. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  131. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  132. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
  133. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
  134. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  135. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
  136. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  137. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
  138. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
  139. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
  140. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
  141. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  142. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
  143. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
  144. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
  145. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
  146. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  147. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
  148. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
  149. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
  150. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
  151. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  152. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
  153. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
  154. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
  155. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
  156. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
  157. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
  158. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
  159. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  160. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  161. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  162. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
  163. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  164. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  165. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
  166. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
  167. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
  168. {npcsh-1.1.22.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
  169. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
  170. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
  171. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
  172. {npcsh-1.1.22.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
@@ -1,26 +1,943 @@
1
- jinx_name: "serve"
2
- description: "Serve an NPC Team"
1
+ jinx_name: serve
2
+ description: NPC Server Dashboard - start, stop, and monitor the API server
3
+ interactive: true
3
4
  inputs:
4
5
  - port: 5337
5
6
  - cors: ""
6
7
  steps:
7
- - name: "start_flask_server"
8
- engine: "python"
8
+ - name: serve_tui
9
+ engine: python
9
10
  code: |
10
- from npcpy.serve import start_flask_server
11
-
12
- port = context.get('port')
13
- cors_str = context.get('cors')
14
- output_messages = context.get('messages', [])
15
-
16
- cors_origins = None
17
- if cors_str and cors_str.strip():
18
- cors_origins = [origin.strip() for origin in cors_str.split(",")]
19
-
20
- start_flask_server(
21
- port=int(port), # Ensure port is an integer
22
- cors_origins=cors_origins,
23
- )
24
-
25
- context['output'] = "NPC Team server started. Execution of this jinx will pause until the server is stopped."
26
- context['messages'] = output_messages
11
+ import os
12
+ import sys
13
+ import time
14
+ import socket
15
+ import threading
16
+ import subprocess
17
+ import signal
18
+
19
+ # ── one-shot fallback ────────────────────────────────────
20
+ if not sys.stdin.isatty():
21
+ from npcpy.serve import start_flask_server
22
+
23
+ port = context.get('port')
24
+ cors_str = context.get('cors')
25
+ output_messages = context.get('messages', [])
26
+
27
+ cors_origins = None
28
+ if cors_str and cors_str.strip():
29
+ cors_origins = [origin.strip() for origin in cors_str.split(",")]
30
+
31
+ start_flask_server(
32
+ port=int(port),
33
+ cors_origins=cors_origins,
34
+ )
35
+ context['output'] = "NPC Team server started."
36
+ context['messages'] = output_messages
37
+
38
+ else:
39
+ import tty
40
+ import termios
41
+ import select
42
+ from collections import deque
43
+
44
+ # ── endpoint registry ───────────────────────────────
45
+ ENDPOINT_REGISTRY = [
46
+ # OpenAI Compatible
47
+ {'category': 'OpenAI Compatible', 'method': 'POST', 'path': '/v1/chat/completions', 'desc': 'OpenAI-compatible chat completions'},
48
+ {'category': 'OpenAI Compatible', 'method': 'GET', 'path': '/v1/models', 'desc': 'List available models (OpenAI format)'},
49
+ # Health
50
+ {'category': 'Health', 'method': 'GET', 'path': '/api/health', 'desc': 'Server health check'},
51
+ # Models
52
+ {'category': 'Models', 'method': 'GET', 'path': '/api/models', 'desc': 'List all available models'},
53
+ {'category': 'Models', 'method': 'GET', 'path': '/api/finetuned_models', 'desc': 'List fine-tuned models'},
54
+ {'category': 'Models', 'method': 'GET', 'path': '/api/instruction_models', 'desc': 'List instruction models'},
55
+ {'category': 'Models', 'method': 'GET', 'path': '/api/image_models', 'desc': 'List image generation models'},
56
+ {'category': 'Models', 'method': 'GET', 'path': '/api/video_models', 'desc': 'List video generation models'},
57
+ {'category': 'Models', 'method': 'GET', 'path': '/api/models/local/scan', 'desc': 'Scan for local models'},
58
+ {'category': 'Models', 'method': 'GET', 'path': '/api/models/hf/scan', 'desc': 'Scan HuggingFace models'},
59
+ {'category': 'Models', 'method': 'POST', 'path': '/api/ollama/pull', 'desc': 'Pull an Ollama model'},
60
+ {'category': 'Models', 'method': 'DELETE', 'path': '/api/ollama/delete', 'desc': 'Delete an Ollama model'},
61
+ # Knowledge Graph
62
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/generations', 'desc': 'List KG generations'},
63
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/graph', 'desc': 'Get full knowledge graph'},
64
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/network-stats', 'desc': 'Network statistics'},
65
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/cooccurrence', 'desc': 'Co-occurrence matrix'},
66
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/centrality', 'desc': 'Node centrality scores'},
67
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/search', 'desc': 'Search knowledge graph'},
68
+ {'category': 'Knowledge Graph', 'method': 'POST', 'path': '/api/kg/embed', 'desc': 'Embed content into KG'},
69
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/search/semantic', 'desc': 'Semantic KG search'},
70
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/facts', 'desc': 'List KG facts'},
71
+ {'category': 'Knowledge Graph', 'method': 'GET', 'path': '/api/kg/concepts', 'desc': 'List KG concepts'},
72
+ # Conversations
73
+ {'category': 'Conversations', 'method': 'GET', 'path': '/api/conversations', 'desc': 'List conversations'},
74
+ {'category': 'Conversations', 'method': 'GET', 'path': '/api/conversation/<id>/messages', 'desc': 'Get messages for conversation'},
75
+ {'category': 'Conversations', 'method': 'GET', 'path': '/api/conversation/<id>/branches', 'desc': 'Get conversation branches'},
76
+ {'category': 'Conversations', 'method': 'POST', 'path': '/api/stream', 'desc': 'Stream a chat response'},
77
+ # Memory
78
+ {'category': 'Conversations', 'method': 'GET', 'path': '/api/memory/list', 'desc': 'List stored memories'},
79
+ {'category': 'Conversations', 'method': 'POST', 'path': '/api/memory/add', 'desc': 'Add a memory'},
80
+ {'category': 'Conversations', 'method': 'DELETE', 'path': '/api/memory/delete', 'desc': 'Delete a memory'},
81
+ # Jinxs
82
+ {'category': 'Jinxs', 'method': 'GET', 'path': '/api/jinxs/available', 'desc': 'List available jinxs'},
83
+ {'category': 'Jinxs', 'method': 'POST', 'path': '/api/jinx/execute', 'desc': 'Execute a jinx'},
84
+ {'category': 'Jinxs', 'method': 'POST', 'path': '/api/jinx/test', 'desc': 'Test a jinx'},
85
+ {'category': 'Jinxs', 'method': 'POST', 'path': '/api/jinxs/save', 'desc': 'Save a jinx'},
86
+ {'category': 'Jinxs', 'method': 'GET', 'path': '/api/jinxs/global', 'desc': 'List global jinxs'},
87
+ {'category': 'Jinxs', 'method': 'GET', 'path': '/api/jinxs/project', 'desc': 'List project jinxs'},
88
+ # NPC Team
89
+ {'category': 'NPC Team', 'method': 'GET', 'path': '/api/npcsh/team', 'desc': 'Get team info'},
90
+ {'category': 'NPC Team', 'method': 'POST', 'path': '/api/npcsh/command', 'desc': 'Execute npcsh command'},
91
+ {'category': 'NPC Team', 'method': 'GET', 'path': '/api/context/current', 'desc': 'Get current context'},
92
+ {'category': 'NPC Team', 'method': 'POST', 'path': '/api/context/switch', 'desc': 'Switch context'},
93
+ # Settings
94
+ {'category': 'Settings', 'method': 'GET', 'path': '/api/settings/global', 'desc': 'Get global settings'},
95
+ {'category': 'Settings', 'method': 'POST', 'path': '/api/settings/global', 'desc': 'Update global settings'},
96
+ {'category': 'Settings', 'method': 'GET', 'path': '/api/settings/project', 'desc': 'Get project settings'},
97
+ {'category': 'Settings', 'method': 'POST', 'path': '/api/settings/project', 'desc': 'Update project settings'},
98
+ {'category': 'Settings', 'method': 'GET', 'path': '/api/last_used_in_npcsh', 'desc': 'Last used model in npcsh'},
99
+ {'category': 'Settings', 'method': 'GET', 'path': '/api/last_used_in_studio', 'desc': 'Last used model in studio'},
100
+ # Audio
101
+ {'category': 'Audio', 'method': 'POST', 'path': '/api/audio/tts', 'desc': 'Text to speech'},
102
+ {'category': 'Audio', 'method': 'POST', 'path': '/api/audio/stt', 'desc': 'Speech to text'},
103
+ {'category': 'Audio', 'method': 'GET', 'path': '/api/audio/stt/engines', 'desc': 'List STT engines'},
104
+ {'category': 'Audio', 'method': 'GET', 'path': '/api/audio/voices', 'desc': 'List available voices'},
105
+ # Media
106
+ {'category': 'Media', 'method': 'POST', 'path': '/api/generate_images', 'desc': 'Generate images'},
107
+ {'category': 'Media', 'method': 'POST', 'path': '/api/generate_video', 'desc': 'Generate video'},
108
+ {'category': 'Media', 'method': 'POST', 'path': '/api/text_predict', 'desc': 'Text prediction'},
109
+ {'category': 'Media', 'method': 'POST', 'path': '/api/generative_fill', 'desc': 'Generative fill on image'},
110
+ {'category': 'Media', 'method': 'POST', 'path': '/api/capture_screenshot', 'desc': 'Capture screenshot'},
111
+ # MCP
112
+ {'category': 'NPC Team', 'method': 'GET', 'path': '/api/mcp_tools', 'desc': 'List MCP tools'},
113
+ {'category': 'NPC Team', 'method': 'POST', 'path': '/api/mcp/server/start', 'desc': 'Start MCP server'},
114
+ {'category': 'NPC Team', 'method': 'POST', 'path': '/api/mcp/server/stop', 'desc': 'Stop MCP server'},
115
+ # Fine-tuning
116
+ {'category': 'Models', 'method': 'POST', 'path': '/api/finetune_diffusers', 'desc': 'Fine-tune diffusion model'},
117
+ {'category': 'Models', 'method': 'GET', 'path': '/api/finetune_status/<job_id>', 'desc': 'Get fine-tune job status'},
118
+ {'category': 'Models', 'method': 'POST', 'path': '/api/finetune_instruction', 'desc': 'Fine-tune instruction model'},
119
+ {'category': 'Models', 'method': 'POST', 'path': '/api/ml/train', 'desc': 'Train ML model'},
120
+ {'category': 'Models', 'method': 'POST', 'path': '/api/ml/predict', 'desc': 'ML prediction'},
121
+ # Studio
122
+ {'category': 'Media', 'method': 'POST', 'path': '/api/studio/action_result', 'desc': 'Get studio action result'},
123
+ ]
124
+
125
+ # ── server thread ──────────────────────────────────
126
+ class ServerThread:
127
+ def __init__(self, app, host, port):
128
+ from werkzeug.serving import make_server
129
+ self.srv = make_server(host, int(port), app)
130
+ self.host = host
131
+ self.port = int(port)
132
+ self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True)
133
+
134
+ def start(self):
135
+ self.thread.start()
136
+
137
+ def stop(self):
138
+ self.srv.shutdown()
139
+
140
+ # ── TUI state ──────────────────────────────────────
141
+ class TUIState:
142
+ def __init__(self):
143
+ self.tab = 0
144
+ self.tabs = ['Server', 'Endpoints', 'Logs']
145
+ self.sel = 0
146
+ self.scroll = 0
147
+ self.search_mode = False
148
+ self.search_buf = ""
149
+ self.search_query = ""
150
+ self.detail = False
151
+ self.detail_lines = []
152
+ self.detail_scroll = 0
153
+ self.status = ""
154
+ self.confirm_action = None
155
+ self.input_mode = False
156
+ self.input_buf = ""
157
+ self.input_label = ""
158
+ self.input_callback = None
159
+ # Server state
160
+ self.server_thread = None
161
+ self.port = int(context.get('port', 5337))
162
+ self.host = '0.0.0.0'
163
+ self.cors = context.get('cors', '') or ''
164
+ self.debug = False
165
+ self.start_time = None
166
+ self.conflict_info = ""
167
+ # Server params for navigation
168
+ self.params = ['port', 'host', 'cors', 'debug']
169
+ # Endpoint list (built once)
170
+ self.endpoints = list(ENDPOINT_REGISTRY)
171
+ self.filtered_endpoints = list(ENDPOINT_REGISTRY)
172
+ # Log buffer
173
+ self.logs = deque(maxlen=500)
174
+ self.log_hook_installed = False
175
+
176
+ ui = TUIState()
177
+
178
+ def term_size():
179
+ try:
180
+ s = os.get_terminal_size()
181
+ return s.columns, s.lines
182
+ except:
183
+ return 80, 24
184
+
185
+ def run_cmd(cmd):
186
+ try:
187
+ r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
188
+ return r.stdout.strip(), r.stderr.strip(), r.returncode
189
+ except Exception as e:
190
+ return "", str(e), 1
191
+
192
+ # ── server helpers ────────────────────────────────
193
+ def is_port_in_use(port):
194
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
195
+ try:
196
+ s.settimeout(1)
197
+ result = s.connect_ex(('127.0.0.1', int(port)))
198
+ return result == 0
199
+ finally:
200
+ s.close()
201
+
202
+ def get_conflict_info(port):
203
+ out, _, rc = run_cmd("lsof -i :" + str(port) + " -P -n 2>/dev/null")
204
+ if rc == 0 and out:
205
+ lines = out.strip().splitlines()
206
+ if len(lines) > 1:
207
+ parts = lines[1].split()
208
+ if len(parts) >= 2:
209
+ return "PID " + parts[1] + " (" + parts[0] + ")"
210
+ out2, _, rc2 = run_cmd("ss -tlnp 'sport = :" + str(port) + "' 2>/dev/null")
211
+ if rc2 == 0 and out2:
212
+ for line in out2.splitlines()[1:]:
213
+ if 'pid=' in line:
214
+ import re
215
+ m = re.search(r'pid=(\d+)', line)
216
+ n = re.search(r'users:\(\("([^"]+)"', line)
217
+ if m:
218
+ info = "PID " + m.group(1)
219
+ if n:
220
+ info += " (" + n.group(1) + ")"
221
+ return info
222
+ return ""
223
+
224
+ def kill_conflict(port):
225
+ out, _, rc = run_cmd("lsof -t -i :" + str(port) + " 2>/dev/null")
226
+ if rc == 0 and out:
227
+ pids = out.strip().splitlines()
228
+ for pid in pids:
229
+ try:
230
+ os.kill(int(pid.strip()), signal.SIGTERM)
231
+ except:
232
+ pass
233
+ ui.status = "Sent SIGTERM to port " + str(port) + " processes"
234
+ ui.conflict_info = ""
235
+ else:
236
+ ui.status = "Could not find process on port " + str(port)
237
+
238
+ def server_running():
239
+ return ui.server_thread is not None and ui.server_thread.thread.is_alive()
240
+
241
+ def uptime_str():
242
+ if not ui.start_time:
243
+ return ""
244
+ elapsed = int(time.time() - ui.start_time)
245
+ h = elapsed // 3600
246
+ m = (elapsed % 3600) // 60
247
+ s = elapsed % 60
248
+ if h > 0:
249
+ return str(h) + "h " + str(m) + "m " + str(s) + "s"
250
+ elif m > 0:
251
+ return str(m) + "m " + str(s) + "s"
252
+ else:
253
+ return str(s) + "s"
254
+
255
+ def install_log_hook(app):
256
+ if ui.log_hook_installed:
257
+ return
258
+ ui.log_hook_installed = True
259
+
260
+ @app.after_request
261
+ def _log_request(response):
262
+ req_time = time.strftime('%H:%M:%S')
263
+ from flask import request as freq
264
+ entry = {
265
+ 'time': req_time,
266
+ 'method': freq.method,
267
+ 'path': freq.path,
268
+ 'status': response.status_code,
269
+ }
270
+ ui.logs.append(entry)
271
+ return response
272
+
273
+ def start_server():
274
+ if server_running():
275
+ ui.status = "Server is already running"
276
+ return
277
+
278
+ # Check port conflict
279
+ if is_port_in_use(ui.port):
280
+ info = get_conflict_info(ui.port)
281
+ ui.conflict_info = info
282
+ ui.status = "Port " + str(ui.port) + " in use" + (" by " + info if info else "")
283
+ return
284
+
285
+ try:
286
+ from npcpy.serve import app, start_flask_server
287
+ from npcpy.serve import CommandHistory
288
+
289
+ cors_str = ui.cors
290
+ cors_origins = None
291
+ if cors_str and cors_str.strip():
292
+ cors_origins = [origin.strip() for origin in cors_str.split(",")]
293
+
294
+ # Configure the app like start_flask_server does
295
+ app.registered_teams = {}
296
+ app.registered_npcs = {}
297
+ app.config['DB_PATH'] = ''
298
+ app.config['user_npc_directory'] = None
299
+
300
+ try:
301
+ db_path = os.environ.get('INCOGNIDE_DB_PATH', os.path.expanduser("~/npcsh_history.db"))
302
+ command_history = CommandHistory(db_path)
303
+ app.command_history = command_history
304
+ app.config['DB_PATH'] = db_path
305
+ user_npc_dir = os.path.expanduser("~/.npcsh/npc_team")
306
+ app.config['user_npc_directory'] = user_npc_dir
307
+ except:
308
+ pass
309
+
310
+ if cors_origins:
311
+ from flask_cors import CORS
312
+ CORS(
313
+ app,
314
+ origins=cors_origins,
315
+ allow_headers=["Content-Type", "Authorization"],
316
+ methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
317
+ supports_credentials=True,
318
+ )
319
+
320
+ install_log_hook(app)
321
+
322
+ ui.server_thread = ServerThread(app, ui.host, ui.port)
323
+ ui.server_thread.start()
324
+ ui.start_time = time.time()
325
+ ui.conflict_info = ""
326
+ ui.status = "Server started on port " + str(ui.port)
327
+ except Exception as e:
328
+ ui.status = "Start failed: " + str(e)[:60]
329
+
330
+ def stop_server():
331
+ if not server_running():
332
+ ui.status = "Server is not running"
333
+ return
334
+ try:
335
+ ui.server_thread.stop()
336
+ ui.server_thread = None
337
+ ui.start_time = None
338
+ ui.status = "Server stopped"
339
+ except Exception as e:
340
+ ui.status = "Stop failed: " + str(e)[:40]
341
+
342
+ def restart_server():
343
+ if server_running():
344
+ stop_server()
345
+ time.sleep(0.5)
346
+ start_server()
347
+
348
+ # ── param helpers ──────────────────────────────────
349
+ def get_param_value(idx):
350
+ p = ui.params[idx]
351
+ if p == 'port':
352
+ return str(ui.port)
353
+ elif p == 'host':
354
+ return ui.host
355
+ elif p == 'cors':
356
+ return ui.cors if ui.cors else "(none)"
357
+ elif p == 'debug':
358
+ return "on" if ui.debug else "off"
359
+ return ""
360
+
361
+ def set_param_value(idx, val):
362
+ p = ui.params[idx]
363
+ if p == 'port':
364
+ try:
365
+ ui.port = int(val)
366
+ ui.status = "Port set to " + val
367
+ except:
368
+ ui.status = "Invalid port number"
369
+ elif p == 'host':
370
+ ui.host = val
371
+ ui.status = "Host set to " + val
372
+ elif p == 'cors':
373
+ ui.cors = val
374
+ ui.status = "CORS set to " + val
375
+ elif p == 'debug':
376
+ ui.debug = val.lower() in ('on', 'true', '1', 'yes')
377
+ ui.status = "Debug " + ("on" if ui.debug else "off")
378
+
379
+ # ── filter endpoints ───────────────────────────────
380
+ def filter_endpoints():
381
+ if ui.search_query:
382
+ q = ui.search_query.lower()
383
+ ui.filtered_endpoints = [e for e in ui.endpoints
384
+ if q in e['path'].lower() or q in e['desc'].lower()
385
+ or q in e['category'].lower() or q in e['method'].lower()]
386
+ else:
387
+ ui.filtered_endpoints = list(ui.endpoints)
388
+
389
+ # ── rendering ──────────────────────────────────────
390
+ def wline(row, text):
391
+ return "\033[" + str(row) + ";1H\033[K" + text
392
+
393
+ def method_color(m):
394
+ m = m.upper()
395
+ if m == 'GET':
396
+ return "\033[32m"
397
+ elif m == 'POST':
398
+ return "\033[33m"
399
+ elif m == 'DELETE':
400
+ return "\033[31m"
401
+ elif m == 'PUT':
402
+ return "\033[34m"
403
+ return ""
404
+
405
+ def status_color(code):
406
+ if 200 <= code < 300:
407
+ return "\033[32m"
408
+ elif 400 <= code < 500:
409
+ return "\033[33m"
410
+ elif code >= 500:
411
+ return "\033[31m"
412
+ return ""
413
+
414
+ def render():
415
+ W, H = term_size()
416
+ out = []
417
+ out.append("\033[H")
418
+
419
+ # ── header ──
420
+ hdr = " SERVE - NPC Server Dashboard "
421
+ out.append(wline(1, "\033[7;1m" + hdr.ljust(W) + "\033[0m"))
422
+
423
+ # ── tabs ──
424
+ tb = ""
425
+ for i, t in enumerate(ui.tabs):
426
+ if i == ui.tab:
427
+ tb += "\033[7;1m [" + t + "] \033[0m"
428
+ else:
429
+ tb += " " + t + " "
430
+ out.append(wline(2, " " + tb))
431
+
432
+ # ── separator + count ──
433
+ out.append(wline(3, "\033[90m" + ("-" * W) + "\033[0m"))
434
+
435
+ if ui.tab == 0:
436
+ running = server_running()
437
+ if running:
438
+ info = " \033[32mRunning\033[0m on \033[1m" + ui.host + ":" + str(ui.port) + "\033[0m"
439
+ ut = uptime_str()
440
+ if ut:
441
+ info += " | uptime: " + ut
442
+ else:
443
+ info = " \033[31mStopped\033[0m"
444
+ if ui.conflict_info:
445
+ info += " | \033[33mPort " + str(ui.port) + " conflict: " + ui.conflict_info + "\033[0m"
446
+ out.append(wline(4, info))
447
+ elif ui.tab == 1:
448
+ ct = len(ui.filtered_endpoints)
449
+ total = len(ui.endpoints)
450
+ if ui.search_query:
451
+ info = " " + str(ct) + " matching (of " + str(total) + ') | search: "' + ui.search_query + '"'
452
+ else:
453
+ info = " " + str(total) + " endpoints"
454
+ out.append(wline(4, info))
455
+ elif ui.tab == 2:
456
+ info = " " + str(len(ui.logs)) + " log entries"
457
+ if server_running():
458
+ info += " | \033[32mlive\033[0m"
459
+ out.append(wline(4, info))
460
+
461
+ out.append(wline(5, "\033[90m" + ("-" * W) + "\033[0m"))
462
+
463
+ # ── body ──
464
+ body_start = 6
465
+ body_end = H - 3
466
+ body_h = max(1, body_end - body_start + 1)
467
+
468
+ if ui.detail:
469
+ render_detail(out, W, body_start, body_h)
470
+ elif ui.input_mode:
471
+ render_input(out, W, body_start, body_h)
472
+ elif ui.tab == 0:
473
+ render_server(out, W, body_start, body_h)
474
+ elif ui.tab == 1:
475
+ render_endpoints(out, W, body_start, body_h)
476
+ elif ui.tab == 2:
477
+ render_logs(out, W, body_start, body_h)
478
+
479
+ # ── separator ──
480
+ out.append(wline(H - 2, "\033[90m" + ("-" * W) + "\033[0m"))
481
+
482
+ # ── status / search / confirm ──
483
+ if ui.confirm_action:
484
+ out.append(wline(H - 1, " \033[33m" + ui.confirm_action[0] + " (y/n)?\033[0m"))
485
+ elif ui.search_mode:
486
+ out.append(wline(H - 1, " \033[33m/\033[0m\033[1m" + ui.search_buf + "\033[0m\033[90m_\033[0m"))
487
+ elif ui.status:
488
+ out.append(wline(H - 1, " \033[33m" + ui.status[:W-2] + "\033[0m"))
489
+ else:
490
+ out.append(wline(H - 1, ""))
491
+
492
+ # ── footer ──
493
+ if ui.confirm_action:
494
+ foot = " [y] Confirm [n] Cancel "
495
+ elif ui.search_mode:
496
+ foot = " [Enter] Apply [Esc] Cancel "
497
+ elif ui.input_mode:
498
+ foot = " Type value, [Enter] Submit [Esc] Cancel "
499
+ elif ui.detail:
500
+ foot = " [j/k] Scroll [q/Esc] Back "
501
+ elif ui.tab == 0:
502
+ foot = " [\xe2\x86\x90\xe2\x86\x92/Tab] Switch [\xe2\x86\x91\xe2\x86\x93/jk] Nav [s] Start [S] Stop [r] Restart [e] Edit [p] Port [K] Kill [q] Quit "
503
+ elif ui.tab == 1:
504
+ foot = " [\xe2\x86\x90\xe2\x86\x92/Tab] Switch [\xe2\x86\x91\xe2\x86\x93/jk] Nav [Enter] Detail [/] Search [q] Quit "
505
+ elif ui.tab == 2:
506
+ foot = " [\xe2\x86\x90\xe2\x86\x92/Tab] Switch [\xe2\x86\x91\xe2\x86\x93/jk] Nav [c] Clear [q] Quit "
507
+ out.append(wline(H, "\033[7m" + foot[:W].ljust(W) + "\033[0m"))
508
+
509
+ sys.stdout.write(''.join(out))
510
+ sys.stdout.flush()
511
+
512
+ def render_server(out, W, start, body_h):
513
+ lines = []
514
+ for i, p in enumerate(ui.params):
515
+ val = get_param_value(i)
516
+ label = p.ljust(10)
517
+ if i == ui.sel:
518
+ lines.append(("\033[7m > " + label + " : " + val + " \033[0m", True))
519
+ else:
520
+ lines.append((" " + "\033[1m" + label + "\033[0m : " + val, False))
521
+
522
+ # Extra info lines after params
523
+ lines.append(("", False))
524
+ running = server_running()
525
+ if running:
526
+ lines.append((" \033[1mURL\033[0m : http://" + ui.host + ":" + str(ui.port), False))
527
+ ut = uptime_str()
528
+ if ut:
529
+ lines.append((" \033[1mUptime\033[0m : " + ut, False))
530
+ else:
531
+ if ui.conflict_info:
532
+ lines.append((" \033[33mPort conflict\033[0m: " + ui.conflict_info, False))
533
+ lines.append((" Press \033[1mK\033[0m to kill the conflicting process", False))
534
+ else:
535
+ # Check if port is in use
536
+ lines.append((" Press \033[1ms\033[0m to start the server", False))
537
+
538
+ if ui.cors and ui.cors.strip():
539
+ lines.append((" \033[1mCORS\033[0m : " + ui.cors, False))
540
+
541
+ for r in range(body_h):
542
+ row = start + r
543
+ if r < len(lines):
544
+ text = lines[r][0]
545
+ is_sel = lines[r][1]
546
+ if is_sel:
547
+ out.append(wline(row, text[:W].ljust(W) if text.startswith("\033[7m") else text[:W]))
548
+ else:
549
+ out.append(wline(row, text[:W]))
550
+ else:
551
+ out.append(wline(row, ""))
552
+
553
+ def render_endpoints(out, W, start, body_h):
554
+ items = ui.filtered_endpoints
555
+ vis = items[ui.scroll:ui.scroll + body_h]
556
+ prev_cat = None
557
+ row_offset = 0
558
+
559
+ for r in range(body_h):
560
+ row = start + r
561
+ idx = r + ui.scroll
562
+ if r >= len(vis):
563
+ out.append(wline(row, ""))
564
+ continue
565
+ ep = vis[r]
566
+ # Category header inline
567
+ mc = method_color(ep['method'])
568
+ method_str = mc + ep['method'].ljust(7) + "\033[0m"
569
+ path_str = ep['path']
570
+ desc_str = ep['desc']
571
+
572
+ if idx == ui.sel:
573
+ line = " > " + ep['method'].ljust(7) + " " + path_str
574
+ remaining = W - len(line) - 3
575
+ if remaining > 5:
576
+ line += " " + desc_str[:remaining]
577
+ out.append(wline(row, "\033[7m" + line[:W].ljust(W) + "\033[0m"))
578
+ else:
579
+ cat_tag = ""
580
+ if r == 0 or (r > 0 and vis[r-1]['category'] != ep['category']):
581
+ cat_tag = "\033[90m[" + ep['category'] + "]\033[0m "
582
+ base = " " + method_str + " " + path_str
583
+ remaining = W - 14 - len(ep['path'])
584
+ if remaining > 5 and not cat_tag:
585
+ base += " \033[90m" + desc_str[:remaining] + "\033[0m"
586
+ elif cat_tag:
587
+ base = " " + cat_tag + method_str + " " + path_str
588
+ out.append(wline(row, base[:W + 40])) # extra for ANSI codes
589
+
590
+ if not items:
591
+ out.append(wline(start, " \033[90mNo endpoints match.\033[0m"))
592
+ for r in range(1, body_h):
593
+ out.append(wline(start + r, ""))
594
+
595
+ def render_logs(out, W, start, body_h):
596
+ # Header row
597
+ hdr = " Time Method Path Status"
598
+ out.append(wline(start, "\033[1m" + hdr[:W] + "\033[0m"))
599
+
600
+ log_list = list(ui.logs)
601
+ log_list.reverse() # newest first
602
+ vis = log_list[ui.scroll:ui.scroll + body_h - 1]
603
+ for r in range(body_h - 1):
604
+ row = start + 1 + r
605
+ idx = r + ui.scroll
606
+ if r >= len(vis):
607
+ out.append(wline(row, ""))
608
+ continue
609
+ entry = vis[r]
610
+ t = entry['time'].ljust(10)
611
+ m = entry['method'].ljust(8)
612
+ p = entry['path'][:34].ljust(34)
613
+ sc = status_color(entry['status'])
614
+ s = sc + str(entry['status']) + "\033[0m"
615
+
616
+ if idx == ui.sel:
617
+ line = " > " + entry['time'].ljust(10) + " " + entry['method'].ljust(8) + " " + entry['path'][:34].ljust(34) + " " + str(entry['status'])
618
+ out.append(wline(row, "\033[7m" + line[:W].ljust(W) + "\033[0m"))
619
+ else:
620
+ out.append(wline(row, " " + t + " " + m + " " + p + " " + s))
621
+
622
+ if not log_list:
623
+ out.append(wline(start + 1, " \033[90mNo log entries yet. Start the server and make requests.\033[0m"))
624
+ for r in range(2, body_h):
625
+ out.append(wline(start + r, ""))
626
+
627
+ def render_detail(out, W, start, body_h):
628
+ vis = ui.detail_lines[ui.detail_scroll:ui.detail_scroll + body_h]
629
+ for r in range(body_h):
630
+ row = start + r
631
+ if r < len(vis):
632
+ out.append(wline(row, " " + vis[r][:W-4]))
633
+ else:
634
+ out.append(wline(row, ""))
635
+
636
+ def render_input(out, W, start, body_h):
637
+ out.append(wline(start, ""))
638
+ out.append(wline(start + 1, " \033[1m" + ui.input_label + "\033[0m"))
639
+ out.append(wline(start + 2, ""))
640
+ out.append(wline(start + 3, " > \033[7m " + ui.input_buf + " \033[0m"))
641
+ for r in range(4, body_h):
642
+ out.append(wline(start + r, ""))
643
+
644
+ # ── endpoint detail ──────────────────────────────
645
+ def show_endpoint_detail():
646
+ items = ui.filtered_endpoints
647
+ if not items or ui.sel >= len(items):
648
+ return
649
+ ep = items[ui.sel]
650
+ ui.detail_lines = [
651
+ "\033[1mEndpoint Detail\033[0m",
652
+ "",
653
+ "\033[1mCategory:\033[0m " + ep['category'],
654
+ "\033[1mMethod:\033[0m " + ep['method'],
655
+ "\033[1mPath:\033[0m " + ep['path'],
656
+ "\033[1mDesc:\033[0m " + ep['desc'],
657
+ "",
658
+ ]
659
+ if server_running():
660
+ url = "http://" + ui.host + ":" + str(ui.port) + ep['path']
661
+ ui.detail_lines.append("\033[1mFull URL:\033[0m " + url)
662
+ ui.detail = True
663
+ ui.detail_scroll = 0
664
+
665
+ # ── input handling ────────────────────────────────
666
+ def handle(c):
667
+ if ui.confirm_action:
668
+ return handle_confirm(c)
669
+ if ui.search_mode:
670
+ return handle_search(c)
671
+ if ui.input_mode:
672
+ return handle_text_input(c)
673
+ if ui.detail:
674
+ return handle_detail(c)
675
+ if c == '\x1b':
676
+ return handle_esc()
677
+
678
+ if c == 'q':
679
+ return False
680
+ elif c == '\t':
681
+ switch_tab(1)
682
+ elif c == 'j':
683
+ nav_down()
684
+ elif c == 'k':
685
+ nav_up()
686
+ elif c in ('\r', '\n'):
687
+ do_enter()
688
+ elif c == '/':
689
+ if ui.tab == 1:
690
+ ui.search_mode = True
691
+ ui.search_buf = ui.search_query
692
+ ui.status = ""
693
+ # Tab-specific keys
694
+ elif ui.tab == 0:
695
+ handle_server_key(c)
696
+ elif ui.tab == 2:
697
+ handle_log_key(c)
698
+ return True
699
+
700
+ def handle_server_key(c):
701
+ if c == 's':
702
+ start_server()
703
+ elif c == 'S':
704
+ stop_server()
705
+ elif c == 'r':
706
+ restart_server()
707
+ elif c == 'e':
708
+ # Edit selected param
709
+ p = ui.params[ui.sel]
710
+ ui.input_mode = True
711
+ ui.input_buf = get_param_value(ui.sel)
712
+ if ui.input_buf == "(none)":
713
+ ui.input_buf = ""
714
+ ui.input_label = "Edit " + p + ":"
715
+ idx = ui.sel
716
+ def on_submit(val):
717
+ set_param_value(idx, val)
718
+ ui.input_callback = on_submit
719
+ elif c == 'p':
720
+ # Quick edit port
721
+ ui.input_mode = True
722
+ ui.input_buf = str(ui.port)
723
+ ui.input_label = "Set port:"
724
+ def on_port(val):
725
+ try:
726
+ ui.port = int(val)
727
+ ui.status = "Port set to " + val
728
+ # Check if new port is in use
729
+ if is_port_in_use(ui.port):
730
+ info = get_conflict_info(ui.port)
731
+ ui.conflict_info = info
732
+ ui.status += " (in use" + (" by " + info if info else "") + ")"
733
+ else:
734
+ ui.conflict_info = ""
735
+ except:
736
+ ui.status = "Invalid port number"
737
+ ui.input_callback = on_port
738
+ elif c == 'K':
739
+ if ui.conflict_info or is_port_in_use(ui.port):
740
+ if not ui.conflict_info:
741
+ ui.conflict_info = get_conflict_info(ui.port) or "unknown"
742
+ ui.confirm_action = ("Kill process on port " + str(ui.port) + " (" + ui.conflict_info + ")",
743
+ lambda: kill_conflict(ui.port))
744
+
745
+ def handle_log_key(c):
746
+ if c == 'c':
747
+ ui.logs.clear()
748
+ ui.sel = 0
749
+ ui.scroll = 0
750
+ ui.status = "Logs cleared"
751
+
752
+ def switch_tab(direction):
753
+ ui.tab = (ui.tab + direction) % len(ui.tabs)
754
+ ui.sel = 0
755
+ ui.scroll = 0
756
+ ui.detail = False
757
+ ui.search_query = ""
758
+ ui.search_buf = ""
759
+ ui.status = ""
760
+ if ui.tab == 1:
761
+ filter_endpoints()
762
+
763
+ def handle_esc():
764
+ if select.select([fd], [], [], 0.05)[0]:
765
+ c2 = os.read(fd, 1).decode('latin-1')
766
+ if c2 == '[':
767
+ c3 = os.read(fd, 1).decode('latin-1')
768
+ if c3 == 'A':
769
+ nav_up()
770
+ elif c3 == 'B':
771
+ nav_down()
772
+ elif c3 == 'C':
773
+ switch_tab(1)
774
+ elif c3 == 'D':
775
+ switch_tab(-1)
776
+ elif c3 == 'Z':
777
+ switch_tab(-1)
778
+ else:
779
+ if ui.search_query:
780
+ ui.search_query = ""
781
+ ui.sel = 0
782
+ ui.scroll = 0
783
+ ui.status = "Search cleared"
784
+ filter_endpoints()
785
+ return True
786
+
787
+ def handle_detail(c):
788
+ if c == '\x1b':
789
+ if select.select([fd], [], [], 0.05)[0]:
790
+ c2 = os.read(fd, 1).decode('latin-1')
791
+ if c2 == '[':
792
+ c3 = os.read(fd, 1).decode('latin-1')
793
+ if c3 == 'A':
794
+ ui.detail_scroll = max(0, ui.detail_scroll - 1)
795
+ elif c3 == 'B':
796
+ ui.detail_scroll += 1
797
+ else:
798
+ ui.detail = False
799
+ ui.detail_scroll = 0
800
+ return True
801
+ if c == 'q':
802
+ ui.detail = False
803
+ ui.detail_scroll = 0
804
+ elif c == 'j':
805
+ ui.detail_scroll += 1
806
+ elif c == 'k':
807
+ ui.detail_scroll = max(0, ui.detail_scroll - 1)
808
+ return True
809
+
810
+ def handle_confirm(c):
811
+ if c == 'y':
812
+ cb = ui.confirm_action[1]
813
+ ui.confirm_action = None
814
+ cb()
815
+ elif c == 'n' or c == '\x1b':
816
+ ui.confirm_action = None
817
+ ui.status = "Cancelled"
818
+ return True
819
+
820
+ def handle_search(c):
821
+ if c == '\x1b':
822
+ if select.select([fd], [], [], 0.05)[0]:
823
+ c2 = os.read(fd, 1).decode('latin-1')
824
+ if c2 == '[':
825
+ os.read(fd, 1).decode('latin-1')
826
+ else:
827
+ ui.search_mode = False
828
+ ui.search_buf = ""
829
+ ui.status = "Search cancelled"
830
+ elif c in ('\r', '\n'):
831
+ ui.search_mode = False
832
+ ui.search_query = ui.search_buf
833
+ ui.search_buf = ""
834
+ ui.sel = 0
835
+ ui.scroll = 0
836
+ filter_endpoints()
837
+ if ui.search_query:
838
+ ui.status = 'Filter: "' + ui.search_query + '" (' + str(len(ui.filtered_endpoints)) + " results)"
839
+ else:
840
+ ui.status = "Search cleared"
841
+ elif c in ('\x7f', '\x08'):
842
+ ui.search_buf = ui.search_buf[:-1]
843
+ elif c == '\x15':
844
+ ui.search_buf = ""
845
+ elif 32 <= ord(c) <= 126:
846
+ ui.search_buf += c
847
+ return True
848
+
849
+ def handle_text_input(c):
850
+ if c == '\x1b':
851
+ if select.select([fd], [], [], 0.05)[0]:
852
+ c2 = os.read(fd, 1).decode('latin-1')
853
+ if c2 == '[':
854
+ os.read(fd, 1).decode('latin-1')
855
+ else:
856
+ ui.input_mode = False
857
+ ui.input_buf = ""
858
+ ui.input_callback = None
859
+ ui.status = "Cancelled"
860
+ elif c in ('\r', '\n'):
861
+ val = ui.input_buf
862
+ cb = ui.input_callback
863
+ ui.input_mode = False
864
+ ui.input_buf = ""
865
+ ui.input_callback = None
866
+ if cb and val.strip():
867
+ cb(val)
868
+ elif c in ('\x7f', '\x08'):
869
+ ui.input_buf = ui.input_buf[:-1]
870
+ elif c == '\x15':
871
+ ui.input_buf = ""
872
+ elif 32 <= ord(c) <= 126:
873
+ ui.input_buf += c
874
+ return True
875
+
876
+ def nav_up():
877
+ if ui.sel > 0:
878
+ ui.sel -= 1
879
+ if ui.sel < ui.scroll:
880
+ ui.scroll = ui.sel
881
+ ui.status = ""
882
+
883
+ def nav_down():
884
+ _, H = term_size()
885
+ body_h = max(1, H - 8)
886
+ if ui.tab == 0:
887
+ mx = max(0, len(ui.params) - 1)
888
+ elif ui.tab == 1:
889
+ mx = max(0, len(ui.filtered_endpoints) - 1)
890
+ elif ui.tab == 2:
891
+ body_h -= 1 # account for header row
892
+ mx = max(0, len(ui.logs) - 1)
893
+ else:
894
+ mx = 0
895
+ if ui.sel < mx:
896
+ ui.sel += 1
897
+ if ui.sel >= ui.scroll + body_h:
898
+ ui.scroll = ui.sel - body_h + 1
899
+ ui.status = ""
900
+
901
+ def do_enter():
902
+ if ui.tab == 0:
903
+ # Edit the selected parameter
904
+ p = ui.params[ui.sel]
905
+ ui.input_mode = True
906
+ ui.input_buf = get_param_value(ui.sel)
907
+ if ui.input_buf == "(none)":
908
+ ui.input_buf = ""
909
+ ui.input_label = "Edit " + p + ":"
910
+ idx = ui.sel
911
+ def on_submit(val):
912
+ set_param_value(idx, val)
913
+ ui.input_callback = on_submit
914
+ elif ui.tab == 1:
915
+ show_endpoint_detail()
916
+
917
+ # ── main loop ──────────────────────────────────────
918
+ fd = sys.stdin.fileno()
919
+ old_attrs = termios.tcgetattr(fd)
920
+
921
+ try:
922
+ tty.setcbreak(fd)
923
+ sys.stdout.write('\033[?25l')
924
+ sys.stdout.write('\033[2J\033[H')
925
+ sys.stdout.flush()
926
+ render()
927
+ while True:
928
+ c = os.read(fd, 1).decode('latin-1')
929
+ if not handle(c):
930
+ break
931
+ render()
932
+ finally:
933
+ # Stop server on exit
934
+ if server_running():
935
+ try:
936
+ ui.server_thread.stop()
937
+ except:
938
+ pass
939
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
940
+ sys.stdout.write('\033[?25h\033[2J\033[H')
941
+ sys.stdout.flush()
942
+
943
+ context['output'] = "Serve dashboard closed."