npcsh 0.1.2__py3-none-any.whl → 1.1.13__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 +3508 -0
- npcsh/alicanto.py +65 -0
- npcsh/build.py +291 -0
- npcsh/completion.py +206 -0
- npcsh/config.py +163 -0
- npcsh/corca.py +50 -0
- npcsh/execution.py +185 -0
- npcsh/guac.py +46 -0
- npcsh/mcp_helpers.py +357 -0
- npcsh/mcp_server.py +299 -0
- npcsh/npc.py +323 -0
- npcsh/npc_team/alicanto.npc +2 -0
- npcsh/npc_team/alicanto.png +0 -0
- npcsh/npc_team/corca.npc +12 -0
- npcsh/npc_team/corca.png +0 -0
- npcsh/npc_team/corca_example.png +0 -0
- npcsh/npc_team/foreman.npc +7 -0
- npcsh/npc_team/frederic.npc +6 -0
- npcsh/npc_team/frederic4.png +0 -0
- npcsh/npc_team/guac.png +0 -0
- npcsh/npc_team/jinxs/code/python.jinx +11 -0
- npcsh/npc_team/jinxs/code/sh.jinx +34 -0
- npcsh/npc_team/jinxs/code/sql.jinx +16 -0
- npcsh/npc_team/jinxs/modes/alicanto.jinx +194 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +249 -0
- npcsh/npc_team/jinxs/modes/guac.jinx +317 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +214 -0
- npcsh/npc_team/jinxs/modes/pti.jinx +170 -0
- npcsh/npc_team/jinxs/modes/spool.jinx +161 -0
- npcsh/npc_team/jinxs/modes/wander.jinx +186 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +262 -0
- npcsh/npc_team/jinxs/npc_studio/npc-studio.jinx +77 -0
- npcsh/npc_team/jinxs/utils/agent.jinx +17 -0
- npcsh/npc_team/jinxs/utils/chat.jinx +44 -0
- npcsh/npc_team/jinxs/utils/cmd.jinx +44 -0
- npcsh/npc_team/jinxs/utils/compress.jinx +140 -0
- npcsh/npc_team/jinxs/utils/core/build.jinx +65 -0
- npcsh/npc_team/jinxs/utils/core/compile.jinx +50 -0
- npcsh/npc_team/jinxs/utils/core/help.jinx +52 -0
- npcsh/npc_team/jinxs/utils/core/init.jinx +41 -0
- npcsh/npc_team/jinxs/utils/core/jinxs.jinx +32 -0
- npcsh/npc_team/jinxs/utils/core/set.jinx +40 -0
- npcsh/npc_team/jinxs/utils/edit_file.jinx +94 -0
- npcsh/npc_team/jinxs/utils/load_file.jinx +35 -0
- npcsh/npc_team/jinxs/utils/ots.jinx +61 -0
- npcsh/npc_team/jinxs/utils/roll.jinx +68 -0
- npcsh/npc_team/jinxs/utils/sample.jinx +56 -0
- npcsh/npc_team/jinxs/utils/search.jinx +130 -0
- npcsh/npc_team/jinxs/utils/serve.jinx +26 -0
- npcsh/npc_team/jinxs/utils/sleep.jinx +116 -0
- npcsh/npc_team/jinxs/utils/trigger.jinx +61 -0
- npcsh/npc_team/jinxs/utils/usage.jinx +33 -0
- npcsh/npc_team/jinxs/utils/vixynt.jinx +144 -0
- npcsh/npc_team/kadiefa.npc +3 -0
- npcsh/npc_team/kadiefa.png +0 -0
- npcsh/npc_team/npcsh.ctx +18 -0
- npcsh/npc_team/npcsh_sibiji.png +0 -0
- npcsh/npc_team/plonk.npc +2 -0
- npcsh/npc_team/plonk.png +0 -0
- npcsh/npc_team/plonkjr.npc +2 -0
- npcsh/npc_team/plonkjr.png +0 -0
- npcsh/npc_team/sibiji.npc +3 -0
- npcsh/npc_team/sibiji.png +0 -0
- npcsh/npc_team/spool.png +0 -0
- npcsh/npc_team/yap.png +0 -0
- npcsh/npcsh.py +296 -112
- npcsh/parsing.py +118 -0
- npcsh/plonk.py +54 -0
- npcsh/pti.py +54 -0
- npcsh/routes.py +139 -0
- npcsh/spool.py +48 -0
- npcsh/ui.py +199 -0
- npcsh/wander.py +62 -0
- npcsh/yap.py +50 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/agent.jinx +17 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.jinx +194 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/build.jinx +65 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/chat.jinx +44 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/cmd.jinx +44 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/compile.jinx +50 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/compress.jinx +140 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.jinx +249 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.npc +12 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca_example.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/edit_file.jinx +94 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/foreman.npc +7 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/frederic.npc +6 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/frederic4.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/guac.jinx +317 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/guac.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/help.jinx +52 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/init.jinx +41 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/jinxs.jinx +32 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.npc +3 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/load_file.jinx +35 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npc-studio.jinx +77 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npcsh.ctx +18 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/ots.jinx +61 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.jinx +214 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/pti.jinx +170 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/python.jinx +11 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/roll.jinx +68 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sample.jinx +56 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/search.jinx +130 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/serve.jinx +26 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/set.jinx +40 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sh.jinx +34 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.npc +3 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sleep.jinx +116 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/spool.jinx +161 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/spool.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sql.jinx +16 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/trigger.jinx +61 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/usage.jinx +33 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/vixynt.jinx +144 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/wander.jinx +186 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/yap.jinx +262 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/yap.png +0 -0
- npcsh-1.1.13.dist-info/METADATA +522 -0
- npcsh-1.1.13.dist-info/RECORD +135 -0
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info}/WHEEL +1 -1
- npcsh-1.1.13.dist-info/entry_points.txt +9 -0
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info/licenses}/LICENSE +1 -1
- npcsh/command_history.py +0 -81
- npcsh/helpers.py +0 -36
- npcsh/llm_funcs.py +0 -295
- npcsh/main.py +0 -5
- npcsh/modes.py +0 -343
- npcsh/npc_compiler.py +0 -124
- npcsh-0.1.2.dist-info/METADATA +0 -99
- npcsh-0.1.2.dist-info/RECORD +0 -14
- npcsh-0.1.2.dist-info/entry_points.txt +0 -2
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info}/top_level.txt +0 -0
npcsh/alicanto.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
alicanto - Deep research mode CLI entry point
|
|
3
|
+
|
|
4
|
+
This is a thin wrapper that executes the alicanto.jinx through the jinx mechanism.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from npcsh._state import setup_shell
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description="alicanto - Deep research with multiple perspectives")
|
|
15
|
+
parser.add_argument("query", nargs="*", help="Research query")
|
|
16
|
+
parser.add_argument("--model", "-m", type=str, help="LLM model to use")
|
|
17
|
+
parser.add_argument("--provider", "-p", type=str, help="LLM provider to use")
|
|
18
|
+
parser.add_argument("--num-npcs", type=int, default=5, help="Number of research perspectives")
|
|
19
|
+
parser.add_argument("--depth", type=int, default=3, help="Research depth")
|
|
20
|
+
parser.add_argument("--max-steps", type=int, default=20, help="Maximum research steps")
|
|
21
|
+
parser.add_argument("--exploration", type=float, default=0.3, help="Exploration factor (0-1)")
|
|
22
|
+
parser.add_argument("--creativity", type=float, default=0.5, help="Creativity factor (0-1)")
|
|
23
|
+
parser.add_argument("--format", type=str, default="report", choices=["report", "summary", "full"],
|
|
24
|
+
help="Output format")
|
|
25
|
+
parser.add_argument("--with-research", action="store_true", help="Include web research")
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
|
|
28
|
+
if not args.query:
|
|
29
|
+
parser.print_help()
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
# Setup shell to get team and default NPC
|
|
33
|
+
command_history, team, default_npc = setup_shell()
|
|
34
|
+
|
|
35
|
+
if not team or "alicanto" not in team.jinxs_dict:
|
|
36
|
+
print("Error: alicanto jinx not found. Ensure npc_team/jinxs/modes/alicanto.jinx exists.")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
# Build context for jinx execution
|
|
40
|
+
context = {
|
|
41
|
+
"npc": default_npc,
|
|
42
|
+
"team": team,
|
|
43
|
+
"messages": [],
|
|
44
|
+
"query": " ".join(args.query),
|
|
45
|
+
"model": args.model,
|
|
46
|
+
"provider": args.provider,
|
|
47
|
+
"num_npcs": args.num_npcs,
|
|
48
|
+
"depth": args.depth,
|
|
49
|
+
"max_steps": args.max_steps,
|
|
50
|
+
"exploration": args.exploration,
|
|
51
|
+
"creativity": args.creativity,
|
|
52
|
+
"format": args.format,
|
|
53
|
+
"skip_research": not args.with_research,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Execute the jinx
|
|
57
|
+
alicanto_jinx = team.jinxs_dict["alicanto"]
|
|
58
|
+
result = alicanto_jinx.execute(context=context, npc=default_npc)
|
|
59
|
+
|
|
60
|
+
if isinstance(result, dict) and result.get("output"):
|
|
61
|
+
print(result["output"])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
npcsh/build.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import textwrap
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_flask_server(config, **kwargs):
|
|
8
|
+
output_dir = Path(config['output_dir'])
|
|
9
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
10
|
+
|
|
11
|
+
server_script = output_dir / 'npc_server.py'
|
|
12
|
+
|
|
13
|
+
server_code = textwrap.dedent(f'''
|
|
14
|
+
import os
|
|
15
|
+
from npcpy.serve import start_flask_server
|
|
16
|
+
from npcpy.npc_compiler import Team
|
|
17
|
+
from sqlalchemy import create_engine
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
team_path = os.path.join(
|
|
21
|
+
os.path.dirname(__file__),
|
|
22
|
+
"npc_team"
|
|
23
|
+
)
|
|
24
|
+
db_path = os.path.expanduser("~/npcsh_history.db")
|
|
25
|
+
|
|
26
|
+
db_conn = create_engine(f'sqlite:///{{db_path}}')
|
|
27
|
+
team = Team(team_path=team_path, db_conn=db_conn)
|
|
28
|
+
|
|
29
|
+
start_flask_server(
|
|
30
|
+
port={config['port']},
|
|
31
|
+
cors_origins={config.get('cors_origins')},
|
|
32
|
+
teams={{"main": team}},
|
|
33
|
+
npcs=team.npcs,
|
|
34
|
+
db_path=db_path,
|
|
35
|
+
user_npc_directory=os.path.expanduser(
|
|
36
|
+
"~/.npcsh/npc_team"
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
''')
|
|
40
|
+
|
|
41
|
+
server_script.write_text(server_code)
|
|
42
|
+
|
|
43
|
+
shutil.copytree(
|
|
44
|
+
config['team_path'],
|
|
45
|
+
output_dir / 'npc_team',
|
|
46
|
+
dirs_exist_ok=True
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
requirements = output_dir / 'requirements.txt'
|
|
50
|
+
requirements.write_text(
|
|
51
|
+
'npcsh\n'
|
|
52
|
+
'flask\n'
|
|
53
|
+
'flask-cors\n'
|
|
54
|
+
'sqlalchemy\n'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
readme = output_dir / 'README.md'
|
|
58
|
+
readme.write_text(textwrap.dedent(f'''
|
|
59
|
+
# NPC Team Server
|
|
60
|
+
|
|
61
|
+
Run: python npc_server.py
|
|
62
|
+
|
|
63
|
+
Server will be available at http://localhost:{config['port']}
|
|
64
|
+
|
|
65
|
+
For pyinstaller standalone:
|
|
66
|
+
pyinstaller --onefile npc_server.py
|
|
67
|
+
'''))
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"output": f"Flask server built in {output_dir}",
|
|
71
|
+
"messages": kwargs.get('messages', [])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_docker_compose(config, **kwargs):
|
|
76
|
+
output_dir = Path(config['output_dir'])
|
|
77
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
shutil.copytree(
|
|
80
|
+
config['team_path'],
|
|
81
|
+
output_dir / 'npc_team',
|
|
82
|
+
dirs_exist_ok=True
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
dockerfile = output_dir / 'Dockerfile'
|
|
86
|
+
dockerfile.write_text(textwrap.dedent('''
|
|
87
|
+
FROM python:3.11-slim
|
|
88
|
+
|
|
89
|
+
WORKDIR /app
|
|
90
|
+
|
|
91
|
+
COPY requirements.txt .
|
|
92
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
93
|
+
|
|
94
|
+
COPY npc_team ./npc_team
|
|
95
|
+
COPY npc_server.py .
|
|
96
|
+
|
|
97
|
+
EXPOSE 5337
|
|
98
|
+
|
|
99
|
+
CMD ["python", "npc_server.py"]
|
|
100
|
+
'''))
|
|
101
|
+
|
|
102
|
+
compose = output_dir / 'docker-compose.yml'
|
|
103
|
+
compose.write_text(textwrap.dedent(f'''
|
|
104
|
+
version: '3.8'
|
|
105
|
+
|
|
106
|
+
services:
|
|
107
|
+
npc-server:
|
|
108
|
+
build: .
|
|
109
|
+
ports:
|
|
110
|
+
- "{config['port']}:{config['port']}"
|
|
111
|
+
volumes:
|
|
112
|
+
- npc-data:/root/.npcsh
|
|
113
|
+
environment:
|
|
114
|
+
- NPCSH_DB_PATH=/root/.npcsh/npcsh_history.db
|
|
115
|
+
|
|
116
|
+
volumes:
|
|
117
|
+
npc-data:
|
|
118
|
+
'''))
|
|
119
|
+
|
|
120
|
+
build_flask_server(config, **kwargs)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"output": f"Docker compose built in {output_dir}. Run: docker-compose up",
|
|
124
|
+
"messages": kwargs.get('messages', [])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_cli_executable(config, **kwargs):
|
|
129
|
+
output_dir = Path(config['output_dir'])
|
|
130
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
|
|
132
|
+
cli_script = output_dir / 'npc_cli.py'
|
|
133
|
+
|
|
134
|
+
cli_code = textwrap.dedent('''
|
|
135
|
+
import sys
|
|
136
|
+
from npcsh._state import setup_shell, execute_command, initial_state
|
|
137
|
+
from npcsh.routes import router
|
|
138
|
+
|
|
139
|
+
def main():
|
|
140
|
+
if len(sys.argv) < 2:
|
|
141
|
+
print("Usage: npc_cli <command>")
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
command = " ".join(sys.argv[1:])
|
|
145
|
+
|
|
146
|
+
command_history, team, npc = setup_shell()
|
|
147
|
+
initial_state.npc = npc
|
|
148
|
+
initial_state.team = team
|
|
149
|
+
|
|
150
|
+
state, result = execute_command(
|
|
151
|
+
command,
|
|
152
|
+
initial_state,
|
|
153
|
+
router=router
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
output = result.get('output') if isinstance(result, dict) else result
|
|
157
|
+
print(output)
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
main()
|
|
161
|
+
''')
|
|
162
|
+
|
|
163
|
+
cli_script.write_text(cli_code)
|
|
164
|
+
|
|
165
|
+
shutil.copytree(
|
|
166
|
+
config['team_path'],
|
|
167
|
+
output_dir / 'npc_team',
|
|
168
|
+
dirs_exist_ok=True
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
spec_file = output_dir / 'npc_cli.spec'
|
|
172
|
+
spec_file.write_text(textwrap.dedent('''
|
|
173
|
+
a = Analysis(
|
|
174
|
+
['npc_cli.py'],
|
|
175
|
+
pathex=[],
|
|
176
|
+
binaries=[],
|
|
177
|
+
datas=[('npc_team', 'npc_team')],
|
|
178
|
+
hiddenimports=[],
|
|
179
|
+
hookspath=[],
|
|
180
|
+
hooksconfig={},
|
|
181
|
+
runtime_hooks=[],
|
|
182
|
+
excludes=[],
|
|
183
|
+
win_no_prefer_redirects=False,
|
|
184
|
+
win_private_assemblies=False,
|
|
185
|
+
cipher=None,
|
|
186
|
+
noarchive=False,
|
|
187
|
+
)
|
|
188
|
+
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
|
|
189
|
+
|
|
190
|
+
exe = EXE(
|
|
191
|
+
pyz,
|
|
192
|
+
a.scripts,
|
|
193
|
+
a.binaries,
|
|
194
|
+
a.zipfiles,
|
|
195
|
+
a.datas,
|
|
196
|
+
[],
|
|
197
|
+
name='npc',
|
|
198
|
+
debug=False,
|
|
199
|
+
bootloader_ignore_signals=False,
|
|
200
|
+
strip=False,
|
|
201
|
+
upx=True,
|
|
202
|
+
upx_exclude=[],
|
|
203
|
+
runtime_tmpdir=None,
|
|
204
|
+
console=True,
|
|
205
|
+
)
|
|
206
|
+
'''))
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"output": (
|
|
210
|
+
f"CLI executable built in {output_dir}. "
|
|
211
|
+
f"Run: pyinstaller npc_cli.spec"
|
|
212
|
+
),
|
|
213
|
+
"messages": kwargs.get('messages', [])
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def build_static_site(config, **kwargs):
|
|
218
|
+
output_dir = Path(config['output_dir'])
|
|
219
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
|
|
221
|
+
html = output_dir / 'index.html'
|
|
222
|
+
html.write_text(textwrap.dedent(f'''
|
|
223
|
+
<!DOCTYPE html>
|
|
224
|
+
<html>
|
|
225
|
+
<head>
|
|
226
|
+
<title>NPC Team Interface</title>
|
|
227
|
+
<style>
|
|
228
|
+
body {{
|
|
229
|
+
font-family: monospace;
|
|
230
|
+
max-width: 800px;
|
|
231
|
+
margin: 50px auto;
|
|
232
|
+
}}
|
|
233
|
+
#output {{
|
|
234
|
+
white-space: pre-wrap;
|
|
235
|
+
background: #f5f5f5;
|
|
236
|
+
padding: 20px;
|
|
237
|
+
min-height: 300px;
|
|
238
|
+
}}
|
|
239
|
+
</style>
|
|
240
|
+
</head>
|
|
241
|
+
<body>
|
|
242
|
+
<h1>NPC Team</h1>
|
|
243
|
+
<input id="input" type="text"
|
|
244
|
+
placeholder="Enter command..."
|
|
245
|
+
style="width: 100%; padding: 10px;">
|
|
246
|
+
<div id="output"></div>
|
|
247
|
+
|
|
248
|
+
<script>
|
|
249
|
+
const API_URL = '{config.get("api_url", "http://localhost:5337")}';
|
|
250
|
+
|
|
251
|
+
document.getElementById('input').addEventListener('keypress',
|
|
252
|
+
async (e) => {{
|
|
253
|
+
if (e.key === 'Enter') {{
|
|
254
|
+
const cmd = e.target.value;
|
|
255
|
+
e.target.value = '';
|
|
256
|
+
|
|
257
|
+
const resp = await fetch(`${{API_URL}}/api/stream`, {{
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {{'Content-Type': 'application/json'}},
|
|
260
|
+
body: JSON.stringify({{
|
|
261
|
+
commandstr: cmd,
|
|
262
|
+
conversationId: 'web-session',
|
|
263
|
+
model: 'llama3.2',
|
|
264
|
+
provider: 'ollama'
|
|
265
|
+
}})
|
|
266
|
+
}});
|
|
267
|
+
|
|
268
|
+
const reader = resp.body.getReader();
|
|
269
|
+
const decoder = new TextDecoder();
|
|
270
|
+
|
|
271
|
+
while (true) {{
|
|
272
|
+
const {{done, value}} = await reader.read();
|
|
273
|
+
if (done) break;
|
|
274
|
+
|
|
275
|
+
const text = decoder.decode(value);
|
|
276
|
+
document.getElementById('output').textContent += text;
|
|
277
|
+
}}
|
|
278
|
+
}}
|
|
279
|
+
}});
|
|
280
|
+
</script>
|
|
281
|
+
</body>
|
|
282
|
+
</html>
|
|
283
|
+
'''))
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"output": (
|
|
287
|
+
f"Static site built in {output_dir}. "
|
|
288
|
+
f"Serve with: python -m http.server 8000"
|
|
289
|
+
),
|
|
290
|
+
"messages": kwargs.get('messages', [])
|
|
291
|
+
}
|
npcsh/completion.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Readline and tab completion for npcsh
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from typing import List, Any, Optional
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import readline
|
|
10
|
+
except ImportError:
|
|
11
|
+
readline = None
|
|
12
|
+
|
|
13
|
+
from .config import READLINE_HISTORY_FILE
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def setup_readline() -> str:
|
|
17
|
+
"""Set up readline with history and completion"""
|
|
18
|
+
if readline is None:
|
|
19
|
+
return ""
|
|
20
|
+
|
|
21
|
+
history_file = READLINE_HISTORY_FILE
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
readline.read_history_file(history_file)
|
|
25
|
+
except FileNotFoundError:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
readline.set_history_length(10000)
|
|
29
|
+
readline.parse_and_bind("tab: complete")
|
|
30
|
+
|
|
31
|
+
return history_file
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_readline_history():
|
|
35
|
+
"""Save readline history to file"""
|
|
36
|
+
if readline is None:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
readline.write_history_file(READLINE_HISTORY_FILE)
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_path_executables() -> List[str]:
|
|
46
|
+
"""Get list of executables in PATH"""
|
|
47
|
+
executables = set()
|
|
48
|
+
|
|
49
|
+
path_dirs = os.environ.get("PATH", "").split(os.pathsep)
|
|
50
|
+
|
|
51
|
+
for path_dir in path_dirs:
|
|
52
|
+
if os.path.isdir(path_dir):
|
|
53
|
+
try:
|
|
54
|
+
for entry in os.listdir(path_dir):
|
|
55
|
+
full_path = os.path.join(path_dir, entry)
|
|
56
|
+
if os.access(full_path, os.X_OK):
|
|
57
|
+
executables.add(entry)
|
|
58
|
+
except PermissionError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
return sorted(executables)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_file_completions(text: str) -> List[str]:
|
|
65
|
+
"""Get file/directory completions for text"""
|
|
66
|
+
completions = []
|
|
67
|
+
|
|
68
|
+
if text.startswith("~"):
|
|
69
|
+
expanded = os.path.expanduser(text)
|
|
70
|
+
prefix = "~"
|
|
71
|
+
search_path = expanded
|
|
72
|
+
else:
|
|
73
|
+
prefix = ""
|
|
74
|
+
search_path = text
|
|
75
|
+
|
|
76
|
+
# Get directory to search
|
|
77
|
+
if os.path.isdir(search_path):
|
|
78
|
+
dir_path = search_path
|
|
79
|
+
name_prefix = ""
|
|
80
|
+
else:
|
|
81
|
+
dir_path = os.path.dirname(search_path) or "."
|
|
82
|
+
name_prefix = os.path.basename(search_path)
|
|
83
|
+
|
|
84
|
+
if not os.path.isdir(dir_path):
|
|
85
|
+
return completions
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
for entry in os.listdir(dir_path):
|
|
89
|
+
if entry.startswith(name_prefix):
|
|
90
|
+
full_path = os.path.join(dir_path, entry)
|
|
91
|
+
if os.path.isdir(full_path):
|
|
92
|
+
completions.append(entry + "/")
|
|
93
|
+
else:
|
|
94
|
+
completions.append(entry)
|
|
95
|
+
except PermissionError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
return completions
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_slash_commands(state: Any, router: Any) -> List[str]:
|
|
102
|
+
"""Get list of available slash commands"""
|
|
103
|
+
commands = set()
|
|
104
|
+
|
|
105
|
+
# Built-in commands and modes
|
|
106
|
+
commands.update([
|
|
107
|
+
'/help', '/set', '/agent', '/chat', '/cmd',
|
|
108
|
+
'/sq', '/quit', '/exit', '/clear',
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
# Team jinxs
|
|
112
|
+
if state.team and hasattr(state.team, 'jinxs_dict'):
|
|
113
|
+
for name in state.team.jinxs_dict:
|
|
114
|
+
commands.add(f'/{name}')
|
|
115
|
+
|
|
116
|
+
# Router jinxs
|
|
117
|
+
if router and hasattr(router, 'jinx_routes'):
|
|
118
|
+
for name in router.jinx_routes:
|
|
119
|
+
commands.add(f'/{name}')
|
|
120
|
+
|
|
121
|
+
return sorted(commands)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_npc_mentions(state: Any) -> List[str]:
|
|
125
|
+
"""Get list of available @npc mentions"""
|
|
126
|
+
npcs = set()
|
|
127
|
+
|
|
128
|
+
# Team NPCs
|
|
129
|
+
if state.team and hasattr(state.team, 'npcs'):
|
|
130
|
+
for name in state.team.npcs:
|
|
131
|
+
npcs.add(f'@{name}')
|
|
132
|
+
|
|
133
|
+
# Also add forenpc if available
|
|
134
|
+
if state.team and hasattr(state.team, 'forenpc') and state.team.forenpc:
|
|
135
|
+
npcs.add(f'@{state.team.forenpc.name}')
|
|
136
|
+
|
|
137
|
+
# Default NPCs if team not loaded yet
|
|
138
|
+
if not npcs:
|
|
139
|
+
npcs.update(['@sibiji', '@guac', '@corca', '@kadiefa', '@plonk', '@forenpc'])
|
|
140
|
+
|
|
141
|
+
return sorted(npcs)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_command_position(buffer: str, begidx: int) -> bool:
|
|
145
|
+
"""Check if we're completing a command (vs argument)"""
|
|
146
|
+
# If we're at the start or after a pipe, it's command position
|
|
147
|
+
before = buffer[:begidx].strip()
|
|
148
|
+
return not before or before.endswith('|')
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def make_completer(shell_state: Any, router: Any):
|
|
152
|
+
"""Create a completer function for readline"""
|
|
153
|
+
|
|
154
|
+
executables = get_path_executables()
|
|
155
|
+
|
|
156
|
+
def completer(text: str, state: int):
|
|
157
|
+
if readline is None:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
buffer = readline.get_line_buffer()
|
|
162
|
+
begidx = readline.get_begidx()
|
|
163
|
+
|
|
164
|
+
# Build completion options
|
|
165
|
+
options = []
|
|
166
|
+
|
|
167
|
+
# Refresh slash commands and NPC mentions each time (they may change)
|
|
168
|
+
slash_commands = get_slash_commands(shell_state, router)
|
|
169
|
+
npc_mentions = get_npc_mentions(shell_state)
|
|
170
|
+
|
|
171
|
+
if text.startswith('/'):
|
|
172
|
+
# Slash command completion
|
|
173
|
+
options = [c for c in slash_commands if c.startswith(text)]
|
|
174
|
+
|
|
175
|
+
elif text.startswith('@'):
|
|
176
|
+
# @npc mention completion
|
|
177
|
+
options = [n for n in npc_mentions if n.startswith(text)]
|
|
178
|
+
|
|
179
|
+
elif text.startswith('~') or '/' in text or text.startswith('.'):
|
|
180
|
+
# File path completion
|
|
181
|
+
options = get_file_completions(text)
|
|
182
|
+
|
|
183
|
+
elif is_command_position(buffer, begidx):
|
|
184
|
+
# Command completion
|
|
185
|
+
options = [e for e in executables if e.startswith(text)]
|
|
186
|
+
|
|
187
|
+
else:
|
|
188
|
+
# Default to file completion
|
|
189
|
+
options = get_file_completions(text)
|
|
190
|
+
|
|
191
|
+
if state < len(options):
|
|
192
|
+
return options[state]
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
except Exception:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
return completer
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def readline_safe_prompt(prompt: str) -> str:
|
|
202
|
+
"""Make prompt safe for readline (escape ANSI codes)"""
|
|
203
|
+
if readline is None:
|
|
204
|
+
return prompt
|
|
205
|
+
# Wrap non-printing characters
|
|
206
|
+
return prompt.replace('\x1b[', '\x01\x1b[').replace('m', 'm\x02')
|