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/routes.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from typing import Callable, Dict, Any, List, Optional
|
|
2
|
+
import functools
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
import sys
|
|
6
|
+
import inspect
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from npcpy.npc_compiler import Jinx, load_jinxs_from_directory, extract_jinx_inputs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommandRouter:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.routes = {}
|
|
15
|
+
self.help_info = {}
|
|
16
|
+
self.jinx_routes = {}
|
|
17
|
+
|
|
18
|
+
def route(self, command: str, help_text: str = "") -> Callable:
|
|
19
|
+
def wrapper(func):
|
|
20
|
+
self.routes[command] = func
|
|
21
|
+
self.help_info[command] = help_text
|
|
22
|
+
|
|
23
|
+
@functools.wraps(func)
|
|
24
|
+
def wrapped_func(*args, **kwargs):
|
|
25
|
+
return func(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
return wrapped_func
|
|
28
|
+
return wrapper
|
|
29
|
+
|
|
30
|
+
def load_jinx_routes(self, jinxs_dir: str):
|
|
31
|
+
if not os.path.exists(jinxs_dir):
|
|
32
|
+
print(f"Jinxs directory not found: {jinxs_dir}")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
for root, dirs, files in os.walk(jinxs_dir):
|
|
36
|
+
for filename in files:
|
|
37
|
+
if filename.endswith('.jinx'):
|
|
38
|
+
jinx_path = os.path.join(root, filename)
|
|
39
|
+
try:
|
|
40
|
+
jinx = Jinx(jinx_path=jinx_path)
|
|
41
|
+
self.register_jinx(jinx)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Error loading jinx {filename}: {e}")
|
|
44
|
+
|
|
45
|
+
def register_jinx(self, jinx: Jinx):
|
|
46
|
+
command_name = jinx.jinx_name
|
|
47
|
+
|
|
48
|
+
def jinx_handler(command: str, **kwargs):
|
|
49
|
+
return self._execute_jinx(jinx, command, **kwargs)
|
|
50
|
+
|
|
51
|
+
self.jinx_routes[command_name] = jinx_handler
|
|
52
|
+
self.help_info[command_name] = jinx.description or "Jinx command"
|
|
53
|
+
|
|
54
|
+
def _execute_jinx(self, jinx: Jinx, command: str, **kwargs):
|
|
55
|
+
messages = kwargs.get("messages", [])
|
|
56
|
+
npc = kwargs.get('npc')
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
import shlex
|
|
60
|
+
|
|
61
|
+
parts = shlex.split(command)
|
|
62
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
63
|
+
|
|
64
|
+
# Use extract_jinx_inputs
|
|
65
|
+
input_values = extract_jinx_inputs(args, jinx)
|
|
66
|
+
|
|
67
|
+
# Build extra_globals for jinx execution
|
|
68
|
+
from npcpy.memory.command_history import CommandHistory, load_kg_from_db
|
|
69
|
+
from npcpy.memory.search import execute_rag_command, execute_brainblast_command
|
|
70
|
+
from npcpy.data.load import load_file_contents
|
|
71
|
+
from npcpy.data.web import search_web
|
|
72
|
+
|
|
73
|
+
application_globals_for_jinx = {
|
|
74
|
+
"CommandHistory": CommandHistory,
|
|
75
|
+
"load_kg_from_db": load_kg_from_db,
|
|
76
|
+
"execute_rag_command": execute_rag_command,
|
|
77
|
+
"execute_brainblast_command": execute_brainblast_command,
|
|
78
|
+
"load_file_contents": load_file_contents,
|
|
79
|
+
"search_web": search_web,
|
|
80
|
+
'state': kwargs.get('state')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Add functions from _state module if available
|
|
84
|
+
try:
|
|
85
|
+
from npcsh import _state
|
|
86
|
+
for name, func in inspect.getmembers(_state, inspect.isfunction):
|
|
87
|
+
application_globals_for_jinx[name] = func
|
|
88
|
+
except:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
jinx_output = jinx.execute(
|
|
92
|
+
input_values=input_values,
|
|
93
|
+
npc=npc,
|
|
94
|
+
messages=messages,
|
|
95
|
+
extra_globals=application_globals_for_jinx
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if isinstance(jinx_output, dict):
|
|
99
|
+
return {
|
|
100
|
+
"output": jinx_output.get('output', str(jinx_output)),
|
|
101
|
+
"messages": jinx_output.get('messages', messages)
|
|
102
|
+
}
|
|
103
|
+
else:
|
|
104
|
+
return {"output": str(jinx_output), "messages": messages}
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
traceback.print_exc()
|
|
108
|
+
return {
|
|
109
|
+
"output": f"Error executing jinx '{jinx.jinx_name}': {e}",
|
|
110
|
+
"messages": messages
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def get_route(self, command: str) -> Optional[Callable]:
|
|
114
|
+
if command in self.routes:
|
|
115
|
+
return self.routes[command]
|
|
116
|
+
elif command in self.jinx_routes:
|
|
117
|
+
return self.jinx_routes[command]
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def execute(self, command_str: str, **kwargs) -> Any:
|
|
121
|
+
command_name = command_str.split()[0].lstrip('/')
|
|
122
|
+
route_func = self.get_route(command_name)
|
|
123
|
+
if route_func:
|
|
124
|
+
return route_func(command=command_str, **kwargs)
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def get_commands(self) -> List[str]:
|
|
128
|
+
all_commands = list(self.routes.keys()) + list(self.jinx_routes.keys())
|
|
129
|
+
return sorted(set(all_commands))
|
|
130
|
+
|
|
131
|
+
def get_help(self, command: str = None) -> Dict[str, str]:
|
|
132
|
+
if command:
|
|
133
|
+
if command in self.help_info:
|
|
134
|
+
return {command: self.help_info[command]}
|
|
135
|
+
return {}
|
|
136
|
+
return self.help_info
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
router = CommandRouter()
|
npcsh/spool.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
spool - Interactive chat mode CLI entry point
|
|
3
|
+
|
|
4
|
+
This is a thin wrapper that executes the spool.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="spool - Interactive chat mode")
|
|
15
|
+
parser.add_argument("--model", "-m", type=str, help="LLM model to use")
|
|
16
|
+
parser.add_argument("--provider", "-p", type=str, help="LLM provider to use")
|
|
17
|
+
parser.add_argument("--files", "-f", nargs="*", help="Files to load for RAG context")
|
|
18
|
+
parser.add_argument("--no-stream", action="store_true", help="Disable streaming")
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
# Setup shell to get team and default NPC
|
|
22
|
+
command_history, team, default_npc = setup_shell()
|
|
23
|
+
|
|
24
|
+
if not team or "spool" not in team.jinxs_dict:
|
|
25
|
+
print("Error: spool jinx not found. Ensure npc_team/jinxs/modes/spool.jinx exists.")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
# Build context for jinx execution
|
|
29
|
+
context = {
|
|
30
|
+
"npc": default_npc,
|
|
31
|
+
"team": team,
|
|
32
|
+
"messages": [],
|
|
33
|
+
"model": args.model,
|
|
34
|
+
"provider": args.provider,
|
|
35
|
+
"attachments": ",".join(args.files) if args.files else None,
|
|
36
|
+
"stream": not args.no_stream,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Execute the jinx
|
|
40
|
+
spool_jinx = team.jinxs_dict["spool"]
|
|
41
|
+
result = spool_jinx.execute(context=context, npc=default_npc)
|
|
42
|
+
|
|
43
|
+
if isinstance(result, dict) and result.get("output"):
|
|
44
|
+
print(result["output"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
npcsh/ui.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI helpers for npcsh - spinners, colors, formatting
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from termcolor import colored
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SpinnerContext:
|
|
11
|
+
"""Context manager for showing a spinner during long operations.
|
|
12
|
+
|
|
13
|
+
Supports ESC key to interrupt (raises KeyboardInterrupt).
|
|
14
|
+
Tracks elapsed time and token counts.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
SPINNER_CHARS = {
|
|
18
|
+
"dots": "⣾⣽⣻⢿⡿⣟⣯⣷",
|
|
19
|
+
"dots_pulse": "⣾⣽⣻⢿⡿⣟⣯⣷",
|
|
20
|
+
"line": "-\\|/",
|
|
21
|
+
"arrow": "←↖↑↗→↘↓↙",
|
|
22
|
+
"brain": "🧠💭💡✨",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, style: str = "dots", delay: float = 0.1):
|
|
26
|
+
self.message = message
|
|
27
|
+
self.style = style
|
|
28
|
+
self.delay = delay
|
|
29
|
+
self.spinner = self.SPINNER_CHARS.get(style, self.SPINNER_CHARS["dots"])
|
|
30
|
+
self._stop = False
|
|
31
|
+
self._thread = None
|
|
32
|
+
self._key_thread = None
|
|
33
|
+
self._interrupted = False
|
|
34
|
+
self._old_settings = None
|
|
35
|
+
self._start_time = None
|
|
36
|
+
self._tokens_in = 0
|
|
37
|
+
self._tokens_out = 0
|
|
38
|
+
self._status_msg = ""
|
|
39
|
+
|
|
40
|
+
def update_tokens(self, tokens_in: int = 0, tokens_out: int = 0):
|
|
41
|
+
"""Update token counts displayed in spinner."""
|
|
42
|
+
self._tokens_in += tokens_in
|
|
43
|
+
self._tokens_out += tokens_out
|
|
44
|
+
|
|
45
|
+
def set_status(self, msg: str):
|
|
46
|
+
"""Set additional status message."""
|
|
47
|
+
self._status_msg = msg
|
|
48
|
+
|
|
49
|
+
def __enter__(self):
|
|
50
|
+
self._stop = False
|
|
51
|
+
self._interrupted = False
|
|
52
|
+
self._start_time = time.time()
|
|
53
|
+
self._thread = threading.Thread(target=self._spin, daemon=True)
|
|
54
|
+
self._thread.start()
|
|
55
|
+
# Start key listener for ESC
|
|
56
|
+
self._key_thread = threading.Thread(target=self._listen_for_esc, daemon=True)
|
|
57
|
+
self._key_thread.start()
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def __exit__(self, *args):
|
|
61
|
+
self._stop = True
|
|
62
|
+
if self._thread:
|
|
63
|
+
self._thread.join(timeout=0.5)
|
|
64
|
+
# Clear spinner line
|
|
65
|
+
sys.stdout.write('\r' + ' ' * (len(self.message) + 20) + '\r')
|
|
66
|
+
sys.stdout.flush()
|
|
67
|
+
# Check if we were interrupted by ESC
|
|
68
|
+
if self._interrupted:
|
|
69
|
+
raise KeyboardInterrupt("ESC pressed")
|
|
70
|
+
|
|
71
|
+
def _listen_for_esc(self):
|
|
72
|
+
"""Listen for ESC key press to interrupt processing."""
|
|
73
|
+
try:
|
|
74
|
+
import termios
|
|
75
|
+
import tty
|
|
76
|
+
import select
|
|
77
|
+
|
|
78
|
+
fd = sys.stdin.fileno()
|
|
79
|
+
self._old_settings = termios.tcgetattr(fd)
|
|
80
|
+
try:
|
|
81
|
+
tty.setcbreak(fd)
|
|
82
|
+
while not self._stop:
|
|
83
|
+
# Check if input is available (non-blocking)
|
|
84
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
85
|
+
ch = sys.stdin.read(1)
|
|
86
|
+
if ch == '\x1b': # ESC key
|
|
87
|
+
self._interrupted = True
|
|
88
|
+
self._stop = True
|
|
89
|
+
break
|
|
90
|
+
finally:
|
|
91
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, self._old_settings)
|
|
92
|
+
except Exception:
|
|
93
|
+
# If we can't set up terminal raw mode (e.g., not a tty), just skip ESC detection
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
def _spin(self):
|
|
97
|
+
idx = 0
|
|
98
|
+
while not self._stop:
|
|
99
|
+
char = self.spinner[idx % len(self.spinner)]
|
|
100
|
+
|
|
101
|
+
# Build status line with timer
|
|
102
|
+
elapsed = time.time() - self._start_time if self._start_time else 0
|
|
103
|
+
mins, secs = divmod(int(elapsed), 60)
|
|
104
|
+
timer_str = f"{mins}:{secs:02d}" if mins else f"{secs}s"
|
|
105
|
+
|
|
106
|
+
# Token info if available
|
|
107
|
+
token_str = ""
|
|
108
|
+
if self._tokens_in or self._tokens_out:
|
|
109
|
+
token_str = colored(f" [{self._tokens_in}→{self._tokens_out} tok]", "cyan")
|
|
110
|
+
|
|
111
|
+
# Additional status
|
|
112
|
+
status_str = ""
|
|
113
|
+
if self._status_msg:
|
|
114
|
+
status_str = colored(f" {self._status_msg}", "yellow")
|
|
115
|
+
|
|
116
|
+
hint = colored(" (ESC to cancel)", "white", attrs=["dark"])
|
|
117
|
+
timer_display = colored(f" [{timer_str}]", "blue")
|
|
118
|
+
|
|
119
|
+
line = f'\r{char} {self.message}...{timer_display}{token_str}{status_str}{hint}'
|
|
120
|
+
# Clear rest of line
|
|
121
|
+
sys.stdout.write(line + ' ' * 10)
|
|
122
|
+
sys.stdout.flush()
|
|
123
|
+
idx += 1
|
|
124
|
+
time.sleep(self.delay)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def show_thinking_animation(message="Thinking", duration=None):
|
|
128
|
+
"""Show a thinking animation for a fixed duration or until interrupted"""
|
|
129
|
+
spinner = SpinnerContext(message)
|
|
130
|
+
with spinner:
|
|
131
|
+
if duration:
|
|
132
|
+
time.sleep(duration)
|
|
133
|
+
else:
|
|
134
|
+
# Run until interrupted
|
|
135
|
+
try:
|
|
136
|
+
while True:
|
|
137
|
+
time.sleep(0.1)
|
|
138
|
+
except KeyboardInterrupt:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def orange(text: str) -> str:
|
|
143
|
+
"""Return text colored orange using colorama"""
|
|
144
|
+
from colorama import Fore, Style
|
|
145
|
+
return f"{Fore.YELLOW}{text}{Style.RESET_ALL}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_file_color(filepath: str) -> tuple:
|
|
149
|
+
"""Get color for file listing based on file type"""
|
|
150
|
+
import os
|
|
151
|
+
from colorama import Fore, Style
|
|
152
|
+
|
|
153
|
+
if os.path.isdir(filepath):
|
|
154
|
+
return Fore.BLUE, Style.BRIGHT
|
|
155
|
+
elif os.path.islink(filepath):
|
|
156
|
+
return Fore.CYAN, ""
|
|
157
|
+
elif os.access(filepath, os.X_OK):
|
|
158
|
+
return Fore.GREEN, Style.BRIGHT
|
|
159
|
+
elif filepath.endswith(('.py', '.sh', '.bash', '.zsh')):
|
|
160
|
+
return Fore.GREEN, ""
|
|
161
|
+
elif filepath.endswith(('.md', '.txt', '.rst')):
|
|
162
|
+
return Fore.WHITE, ""
|
|
163
|
+
elif filepath.endswith(('.json', '.yaml', '.yml', '.toml')):
|
|
164
|
+
return Fore.YELLOW, ""
|
|
165
|
+
elif filepath.endswith(('.jpg', '.png', '.gif', '.svg', '.ico')):
|
|
166
|
+
return Fore.MAGENTA, ""
|
|
167
|
+
else:
|
|
168
|
+
return "", ""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def format_file_listing(output: str) -> str:
|
|
172
|
+
"""Format file listing output with colors"""
|
|
173
|
+
import os
|
|
174
|
+
from colorama import Style
|
|
175
|
+
|
|
176
|
+
lines = output.strip().split('\n')
|
|
177
|
+
formatted = []
|
|
178
|
+
|
|
179
|
+
for line in lines:
|
|
180
|
+
if not line.strip():
|
|
181
|
+
formatted.append(line)
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
# Try to color the file part
|
|
185
|
+
parts = line.rsplit('/', 1)
|
|
186
|
+
if len(parts) == 2:
|
|
187
|
+
path, filename = parts
|
|
188
|
+
fg, style = get_file_color(line)
|
|
189
|
+
formatted.append(f"{path}/{fg}{style}{filename}{Style.RESET_ALL}")
|
|
190
|
+
else:
|
|
191
|
+
formatted.append(line)
|
|
192
|
+
|
|
193
|
+
return '\n'.join(formatted)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def wrap_text(text: str, width: int = 80) -> str:
|
|
197
|
+
"""Wrap text to specified width"""
|
|
198
|
+
import textwrap
|
|
199
|
+
return textwrap.fill(text, width=width)
|
npcsh/wander.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
wander - Experimental wandering mode CLI entry point
|
|
3
|
+
|
|
4
|
+
This is a thin wrapper that executes the wander.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="wander - Creative exploration with varied temperatures")
|
|
15
|
+
parser.add_argument("problem", nargs="*", help="Problem to explore through wandering")
|
|
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("--environment", type=str, help="Metaphorical environment for wandering")
|
|
19
|
+
parser.add_argument("--low-temp", type=float, default=0.5, help="Low temperature setting")
|
|
20
|
+
parser.add_argument("--high-temp", type=float, default=1.9, help="High temperature setting")
|
|
21
|
+
parser.add_argument("--n-streams", type=int, default=5, help="Number of exploration streams")
|
|
22
|
+
parser.add_argument("--include-events", action="store_true", help="Include random events")
|
|
23
|
+
parser.add_argument("--num-events", type=int, default=3, help="Number of events per stream")
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
if not args.problem:
|
|
27
|
+
parser.print_help()
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
# Setup shell to get team and default NPC
|
|
31
|
+
command_history, team, default_npc = setup_shell()
|
|
32
|
+
|
|
33
|
+
if not team or "wander" not in team.jinxs_dict:
|
|
34
|
+
print("Error: wander jinx not found. Ensure npc_team/jinxs/modes/wander.jinx exists.")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
# Build context for jinx execution
|
|
38
|
+
context = {
|
|
39
|
+
"npc": default_npc,
|
|
40
|
+
"team": team,
|
|
41
|
+
"messages": [],
|
|
42
|
+
"problem": " ".join(args.problem),
|
|
43
|
+
"model": args.model,
|
|
44
|
+
"provider": args.provider,
|
|
45
|
+
"environment": args.environment,
|
|
46
|
+
"low_temp": args.low_temp,
|
|
47
|
+
"high_temp": args.high_temp,
|
|
48
|
+
"n_streams": args.n_streams,
|
|
49
|
+
"include_events": args.include_events,
|
|
50
|
+
"num_events": args.num_events,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Execute the jinx
|
|
54
|
+
wander_jinx = team.jinxs_dict["wander"]
|
|
55
|
+
result = wander_jinx.execute(context=context, npc=default_npc)
|
|
56
|
+
|
|
57
|
+
if isinstance(result, dict) and result.get("output"):
|
|
58
|
+
print(result["output"])
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
npcsh/yap.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
yap - Voice chat mode CLI entry point
|
|
3
|
+
|
|
4
|
+
This is a thin wrapper that executes the yap.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="yap - Voice chat mode")
|
|
15
|
+
parser.add_argument("--model", "-m", type=str, help="LLM model to use")
|
|
16
|
+
parser.add_argument("--provider", "-p", type=str, help="LLM provider to use")
|
|
17
|
+
parser.add_argument("--files", "-f", nargs="*", help="Files to load for RAG context")
|
|
18
|
+
parser.add_argument("--tts-model", type=str, default="kokoro", help="TTS model to use")
|
|
19
|
+
parser.add_argument("--voice", type=str, default="af_heart", help="Voice for TTS")
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
# Setup shell to get team and default NPC
|
|
23
|
+
command_history, team, default_npc = setup_shell()
|
|
24
|
+
|
|
25
|
+
if not team or "yap" not in team.jinxs_dict:
|
|
26
|
+
print("Error: yap jinx not found. Ensure npc_team/jinxs/modes/yap.jinx exists.")
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
# Build context for jinx execution
|
|
30
|
+
context = {
|
|
31
|
+
"npc": default_npc,
|
|
32
|
+
"team": team,
|
|
33
|
+
"messages": [],
|
|
34
|
+
"model": args.model,
|
|
35
|
+
"provider": args.provider,
|
|
36
|
+
"files": ",".join(args.files) if args.files else None,
|
|
37
|
+
"tts_model": args.tts_model,
|
|
38
|
+
"voice": args.voice,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Execute the jinx
|
|
42
|
+
yap_jinx = team.jinxs_dict["yap"]
|
|
43
|
+
result = yap_jinx.execute(context=context, npc=default_npc)
|
|
44
|
+
|
|
45
|
+
if isinstance(result, dict) and result.get("output"):
|
|
46
|
+
print(result["output"])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
jinx_name: agent
|
|
2
|
+
description: Provides an LLM response with tool use enabled.
|
|
3
|
+
inputs:
|
|
4
|
+
- query
|
|
5
|
+
- auto_process_tool_calls: True
|
|
6
|
+
- use_core_tools: True
|
|
7
|
+
steps:
|
|
8
|
+
- name: get_agent_response
|
|
9
|
+
engine: python
|
|
10
|
+
code: |
|
|
11
|
+
response = npc.get_llm_response(
|
|
12
|
+
request=query,
|
|
13
|
+
messages=context.get('messages', []),
|
|
14
|
+
auto_process_tool_calls={{ auto_process_tool_calls | default(True) }},
|
|
15
|
+
use_core_tools={{ use_core_tools | default(True) }}
|
|
16
|
+
)
|
|
17
|
+
output = response.get('response', '')
|