npcsh 1.1.21__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.
- npcsh/_state.py +282 -125
- npcsh/benchmark/npcsh_agent.py +77 -232
- npcsh/benchmark/templates/install-npcsh.sh.j2 +12 -4
- npcsh/config.py +5 -2
- npcsh/mcp_server.py +9 -1
- npcsh/npc_team/alicanto.npc +8 -6
- npcsh/npc_team/corca.npc +5 -12
- npcsh/npc_team/frederic.npc +6 -9
- npcsh/npc_team/guac.npc +4 -4
- npcsh/npc_team/jinxs/lib/core/delegate.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/edit_file.jinx +84 -62
- npcsh/npc_team/jinxs/lib/core/sh.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/skill.jinx +59 -0
- npcsh/npc_team/jinxs/lib/utils/help.jinx +194 -10
- npcsh/npc_team/jinxs/lib/utils/init.jinx +528 -37
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +0 -1
- npcsh/npc_team/jinxs/lib/utils/serve.jinx +938 -21
- npcsh/npc_team/jinxs/modes/alicanto.jinx +102 -41
- npcsh/npc_team/jinxs/modes/build.jinx +378 -0
- npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx → npcsh/npc_team/jinxs/modes/config.jinx +1 -1
- npcsh/npc_team/jinxs/modes/convene.jinx +670 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
- npcsh/npc_team/jinxs/modes/crond.jinx +818 -0
- npcsh/npc_team/jinxs/modes/kg.jinx +69 -2
- npcsh/npc_team/jinxs/modes/plonk.jinx +86 -15
- npcsh/npc_team/jinxs/modes/roll.jinx +368 -55
- npcsh/npc_team/jinxs/modes/skills.jinx +621 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +1092 -177
- npcsh/npc_team/jinxs/skills/code-review/SKILL.md +45 -0
- npcsh/npc_team/jinxs/skills/debugging/SKILL.md +44 -0
- npcsh/npc_team/jinxs/skills/git-workflow.jinx +44 -0
- npcsh/npc_team/kadiefa.npc +6 -6
- npcsh/npc_team/npcsh.ctx +16 -0
- npcsh/npc_team/plonk.npc +5 -9
- npcsh/npc_team/sibiji.npc +15 -7
- npcsh/npcsh.py +1 -0
- npcsh/routes.py +0 -4
- npcsh/yap.py +22 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/SKILL.md +44 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.jinx +102 -41
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.npc +8 -6
- npcsh-1.1.23.data/data/npcsh/npc_team/build.jinx +378 -0
- npcsh/npc_team/jinxs/modes/config_tui.jinx → npcsh-1.1.23.data/data/npcsh/npc_team/config.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/convene.jinx +670 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/corca.jinx +820 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.npc +5 -12
- npcsh-1.1.23.data/data/npcsh/npc_team/crond.jinx +818 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/delegate.jinx +1 -1
- npcsh-1.1.23.data/data/npcsh/npc_team/edit_file.jinx +119 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic.npc +6 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/git-workflow.jinx +44 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.npc +4 -4
- npcsh-1.1.23.data/data/npcsh/npc_team/help.jinx +236 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/init.jinx +532 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/jinxs.jinx +0 -1
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.npc +6 -6
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kg.jinx +69 -2
- npcsh-1.1.23.data/data/npcsh/npc_team/npcsh.ctx +34 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.jinx +86 -15
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.npc +5 -9
- npcsh-1.1.23.data/data/npcsh/npc_team/roll.jinx +378 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/serve.jinx +943 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sh.jinx +1 -1
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.npc +15 -7
- npcsh-1.1.23.data/data/npcsh/npc_team/skill.jinx +59 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/skills.jinx +621 -0
- npcsh-1.1.23.data/data/npcsh/npc_team/yap.jinx +1190 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/METADATA +404 -278
- npcsh-1.1.23.dist-info/RECORD +216 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/confirm.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +0 -8
- npcsh/npc_team/jinxs/incognide/navigate.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/notify.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +0 -13
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +0 -9
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/send_message.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +0 -12
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +0 -10
- npcsh/npc_team/jinxs/incognide/write_file.jinx +0 -11
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +0 -9
- npcsh/npc_team/jinxs/lib/core/convene.jinx +0 -232
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -429
- npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
- npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/add_tab.jinx +0 -11
- npcsh-1.1.21.data/data/npcsh/npc_team/build.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/close_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/close_tab.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/confirm.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/convene.jinx +0 -232
- npcsh-1.1.21.data/data/npcsh/npc_team/corca.jinx +0 -430
- npcsh-1.1.21.data/data/npcsh/npc_team/edit_file.jinx +0 -97
- npcsh-1.1.21.data/data/npcsh/npc_team/focus_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/help.jinx +0 -52
- npcsh-1.1.21.data/data/npcsh/npc_team/init.jinx +0 -41
- npcsh-1.1.21.data/data/npcsh/npc_team/kg_search.jinx +0 -429
- npcsh-1.1.21.data/data/npcsh/npc_team/list_panes.jinx +0 -8
- npcsh-1.1.21.data/data/npcsh/npc_team/navigate.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/notify.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/npcsh.ctx +0 -18
- npcsh-1.1.21.data/data/npcsh/npc_team/open_pane.jinx +0 -13
- npcsh-1.1.21.data/data/npcsh/npc_team/read_pane.jinx +0 -9
- npcsh-1.1.21.data/data/npcsh/npc_team/roll.jinx +0 -65
- npcsh-1.1.21.data/data/npcsh/npc_team/run_terminal.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/search.jinx +0 -54
- npcsh-1.1.21.data/data/npcsh/npc_team/send_message.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/serve.jinx +0 -26
- npcsh-1.1.21.data/data/npcsh/npc_team/split_pane.jinx +0 -12
- npcsh-1.1.21.data/data/npcsh/npc_team/switch_npc.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/switch_tab.jinx +0 -10
- npcsh-1.1.21.data/data/npcsh/npc_team/write_file.jinx +0 -11
- npcsh-1.1.21.data/data/npcsh/npc_team/yap.jinx +0 -275
- npcsh-1.1.21.data/data/npcsh/npc_team/zen_mode.jinx +0 -9
- npcsh-1.1.21.dist-info/RECORD +0 -243
- /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
- /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
- /npcsh/npc_team/jinxs/{incognide → lib/utils}/incognide.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/db_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/file_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/git.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/memories.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/models.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/papers.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/pti.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/reattach.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/setup.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/sync.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/team.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/wander.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/web_search.jinx +0 -0
- {npcsh-1.1.21.data → npcsh-1.1.23.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/WHEEL +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/entry_points.txt +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.21.dist-info → npcsh-1.1.23.dist-info}/top_level.txt +0 -0
|
@@ -1,26 +1,943 @@
|
|
|
1
|
-
jinx_name:
|
|
2
|
-
description:
|
|
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:
|
|
8
|
-
engine:
|
|
8
|
+
- name: serve_tui
|
|
9
|
+
engine: python
|
|
9
10
|
code: |
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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."
|