npcsh 1.0.26__py3-none-any.whl → 1.0.28__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 +115 -111
- npcsh/alicanto.py +88 -88
- npcsh/corca.py +423 -95
- npcsh/guac.py +110 -107
- npcsh/mcp_helpers.py +45 -45
- npcsh/mcp_server.py +16 -17
- npcsh/npc.py +16 -17
- npcsh/npc_team/jinxs/bash_executer.jinx +1 -1
- npcsh/npc_team/jinxs/edit_file.jinx +6 -6
- npcsh/npc_team/jinxs/image_generation.jinx +5 -5
- npcsh/npc_team/jinxs/screen_cap.jinx +2 -2
- npcsh/npcsh.py +15 -6
- npcsh/plonk.py +8 -8
- npcsh/routes.py +77 -77
- npcsh/spool.py +13 -13
- npcsh/wander.py +37 -37
- npcsh/yap.py +72 -72
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/bash_executer.jinx +1 -1
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/edit_file.jinx +6 -6
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/image_generation.jinx +5 -5
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/screen_cap.jinx +2 -2
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/METADATA +1 -1
- npcsh-1.0.28.dist-info/RECORD +73 -0
- npcsh-1.0.26.dist-info/RECORD +0 -73
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/foreman.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/internet_search.jinx +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/python_executor.jinx +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.0.26.data → npcsh-1.0.28.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/WHEEL +0 -0
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/entry_points.txt +0 -0
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.0.26.dist-info → npcsh-1.0.28.dist-info}/top_level.txt +0 -0
npcsh/_state.py
CHANGED
|
@@ -5,11 +5,18 @@ import filecmp
|
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import platform
|
|
8
|
-
import pty
|
|
9
8
|
try:
|
|
9
|
+
import pty
|
|
10
|
+
import tty
|
|
11
|
+
|
|
12
|
+
import termios
|
|
13
|
+
|
|
10
14
|
import readline
|
|
11
15
|
except:
|
|
12
|
-
|
|
16
|
+
readline = None
|
|
17
|
+
pty = None
|
|
18
|
+
tty = None
|
|
19
|
+
|
|
13
20
|
import re
|
|
14
21
|
import select
|
|
15
22
|
import shlex
|
|
@@ -18,11 +25,8 @@ import signal
|
|
|
18
25
|
import sqlite3
|
|
19
26
|
import subprocess
|
|
20
27
|
import sys
|
|
21
|
-
from termcolor import colored
|
|
22
|
-
import termios
|
|
23
28
|
import time
|
|
24
29
|
from typing import Dict, List, Any, Tuple, Union, Optional
|
|
25
|
-
import tty
|
|
26
30
|
import logging
|
|
27
31
|
import textwrap
|
|
28
32
|
from termcolor import colored
|
|
@@ -104,16 +108,16 @@ except importlib.metadata.PackageNotFoundError:
|
|
|
104
108
|
|
|
105
109
|
|
|
106
110
|
NPCSH_CHAT_MODEL = os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b")
|
|
107
|
-
|
|
111
|
+
|
|
108
112
|
NPCSH_CHAT_PROVIDER = os.environ.get("NPCSH_CHAT_PROVIDER", "ollama")
|
|
109
|
-
|
|
113
|
+
|
|
110
114
|
NPCSH_DB_PATH = os.path.expanduser(
|
|
111
115
|
os.environ.get("NPCSH_DB_PATH", "~/npcsh_history.db")
|
|
112
116
|
)
|
|
113
117
|
NPCSH_VECTOR_DB_PATH = os.path.expanduser(
|
|
114
118
|
os.environ.get("NPCSH_VECTOR_DB_PATH", "~/npcsh_chroma.db")
|
|
115
119
|
)
|
|
116
|
-
|
|
120
|
+
|
|
117
121
|
|
|
118
122
|
NPCSH_DEFAULT_MODE = os.path.expanduser(os.environ.get("NPCSH_DEFAULT_MODE", "agent"))
|
|
119
123
|
NPCSH_VISION_MODEL = os.environ.get("NPCSH_VISION_MODEL", "gemma3:4b")
|
|
@@ -181,45 +185,45 @@ class ShellState:
|
|
|
181
185
|
elif model_type == "video_gen":
|
|
182
186
|
return self.video_gen_model, self.video_gen_provider
|
|
183
187
|
else:
|
|
184
|
-
return self.chat_model, self.chat_provider
|
|
188
|
+
return self.chat_model, self.chat_provider
|
|
185
189
|
CONFIG_KEY_MAP = {
|
|
186
|
-
|
|
190
|
+
|
|
187
191
|
"model": "NPCSH_CHAT_MODEL",
|
|
188
192
|
"chatmodel": "NPCSH_CHAT_MODEL",
|
|
189
193
|
"provider": "NPCSH_CHAT_PROVIDER",
|
|
190
194
|
"chatprovider": "NPCSH_CHAT_PROVIDER",
|
|
191
195
|
|
|
192
|
-
|
|
196
|
+
|
|
193
197
|
"vmodel": "NPCSH_VISION_MODEL",
|
|
194
198
|
"visionmodel": "NPCSH_VISION_MODEL",
|
|
195
199
|
"vprovider": "NPCSH_VISION_PROVIDER",
|
|
196
200
|
"visionprovider": "NPCSH_VISION_PROVIDER",
|
|
197
201
|
|
|
198
|
-
|
|
202
|
+
|
|
199
203
|
"emodel": "NPCSH_EMBEDDING_MODEL",
|
|
200
204
|
"embeddingmodel": "NPCSH_EMBEDDING_MODEL",
|
|
201
205
|
"eprovider": "NPCSH_EMBEDDING_PROVIDER",
|
|
202
206
|
"embeddingprovider": "NPCSH_EMBEDDING_PROVIDER",
|
|
203
207
|
|
|
204
|
-
|
|
208
|
+
|
|
205
209
|
"rmodel": "NPCSH_REASONING_MODEL",
|
|
206
210
|
"reasoningmodel": "NPCSH_REASONING_MODEL",
|
|
207
211
|
"rprovider": "NPCSH_REASONING_PROVIDER",
|
|
208
212
|
"reasoningprovider": "NPCSH_REASONING_PROVIDER",
|
|
209
213
|
|
|
210
|
-
|
|
214
|
+
|
|
211
215
|
"igmodel": "NPCSH_IMAGE_GEN_MODEL",
|
|
212
216
|
"imagegenmodel": "NPCSH_IMAGE_GEN_MODEL",
|
|
213
217
|
"igprovider": "NPCSH_IMAGE_GEN_PROVIDER",
|
|
214
218
|
"imagegenprovider": "NPCSH_IMAGE_GEN_PROVIDER",
|
|
215
219
|
|
|
216
|
-
|
|
220
|
+
|
|
217
221
|
"vgmodel": "NPCSH_VIDEO_GEN_MODEL",
|
|
218
222
|
"videogenmodel": "NPCSH_VIDEO_GEN_MODEL",
|
|
219
223
|
"vgprovider": "NPCSH_VIDEO_GEN_PROVIDER",
|
|
220
224
|
"videogenprovider": "NPCSH_VIDEO_GEN_PROVIDER",
|
|
221
225
|
|
|
222
|
-
|
|
226
|
+
|
|
223
227
|
"sprovider": "NPCSH_SEARCH_PROVIDER",
|
|
224
228
|
"mode": "NPCSH_DEFAULT_MODE",
|
|
225
229
|
"stream": "NPCSH_STREAM_OUTPUT",
|
|
@@ -233,13 +237,13 @@ def set_npcsh_config_value(key: str, value: str):
|
|
|
233
237
|
Set NPCSH config values at runtime using shorthand (case-insensitive) or full keys.
|
|
234
238
|
Updates os.environ, globals, and ShellState defaults.
|
|
235
239
|
"""
|
|
236
|
-
|
|
240
|
+
|
|
237
241
|
env_key = CONFIG_KEY_MAP.get(key.lower(), key)
|
|
238
242
|
|
|
239
|
-
|
|
243
|
+
|
|
240
244
|
os.environ[env_key] = value
|
|
241
245
|
|
|
242
|
-
|
|
246
|
+
|
|
243
247
|
if env_key in ["NPCSH_STREAM_OUTPUT", "NPCSH_BUILD_KG"]:
|
|
244
248
|
parsed_val = value.strip().lower() in ["1", "true", "yes"]
|
|
245
249
|
elif env_key.endswith("_PATH"):
|
|
@@ -247,10 +251,10 @@ def set_npcsh_config_value(key: str, value: str):
|
|
|
247
251
|
else:
|
|
248
252
|
parsed_val = value
|
|
249
253
|
|
|
250
|
-
|
|
254
|
+
|
|
251
255
|
globals()[env_key] = parsed_val
|
|
252
256
|
|
|
253
|
-
|
|
257
|
+
|
|
254
258
|
field_map = {
|
|
255
259
|
"NPCSH_CHAT_MODEL": "chat_model",
|
|
256
260
|
"NPCSH_CHAT_PROVIDER": "chat_provider",
|
|
@@ -298,7 +302,7 @@ def get_npc_path(npc_name: str, db_path: str) -> str:
|
|
|
298
302
|
except Exception as e:
|
|
299
303
|
print(f"Database query error: {e}")
|
|
300
304
|
|
|
301
|
-
|
|
305
|
+
|
|
302
306
|
if os.path.exists(project_npc_path):
|
|
303
307
|
return project_npc_path
|
|
304
308
|
|
|
@@ -327,7 +331,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
327
331
|
conn = sqlite3.connect(db_path)
|
|
328
332
|
cursor = conn.cursor()
|
|
329
333
|
|
|
330
|
-
|
|
334
|
+
|
|
331
335
|
cursor.execute(
|
|
332
336
|
"""
|
|
333
337
|
CREATE TABLE IF NOT EXISTS compiled_npcs (
|
|
@@ -338,7 +342,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
338
342
|
"""
|
|
339
343
|
)
|
|
340
344
|
|
|
341
|
-
|
|
345
|
+
|
|
342
346
|
package_dir = os.path.dirname(__file__)
|
|
343
347
|
package_npc_team_dir = os.path.join(package_dir, "npc_team")
|
|
344
348
|
|
|
@@ -368,7 +372,7 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
368
372
|
shutil.copy2(source_path, destination_path)
|
|
369
373
|
print(f"Copied ctx {filename} to {destination_path}")
|
|
370
374
|
|
|
371
|
-
|
|
375
|
+
|
|
372
376
|
package_jinxs_dir = os.path.join(package_npc_team_dir, "jinxs")
|
|
373
377
|
if os.path.exists(package_jinxs_dir):
|
|
374
378
|
for filename in os.listdir(package_jinxs_dir):
|
|
@@ -416,19 +420,19 @@ def get_shell_config_file() -> str:
|
|
|
416
420
|
Returns:
|
|
417
421
|
The path to the shell configuration file.
|
|
418
422
|
"""
|
|
419
|
-
|
|
423
|
+
|
|
420
424
|
shell = os.environ.get("SHELL", "")
|
|
421
425
|
|
|
422
426
|
if "zsh" in shell:
|
|
423
427
|
return os.path.expanduser("~/.zshrc")
|
|
424
428
|
elif "bash" in shell:
|
|
425
|
-
|
|
429
|
+
|
|
426
430
|
if platform.system() == "Darwin":
|
|
427
431
|
return os.path.expanduser("~/.bash_profile")
|
|
428
432
|
else:
|
|
429
433
|
return os.path.expanduser("~/.bashrc")
|
|
430
434
|
else:
|
|
431
|
-
|
|
435
|
+
|
|
432
436
|
return os.path.expanduser("~/.bashrc")
|
|
433
437
|
|
|
434
438
|
|
|
@@ -569,14 +573,14 @@ def get_argument_help() -> Dict[str, List[str]]:
|
|
|
569
573
|
arg_map = {arg: [] for arg in CANONICAL_ARGS}
|
|
570
574
|
|
|
571
575
|
for arg in CANONICAL_ARGS:
|
|
572
|
-
|
|
576
|
+
|
|
573
577
|
for i in range(1, len(arg)):
|
|
574
578
|
prefix = arg[:i]
|
|
575
579
|
|
|
576
|
-
|
|
580
|
+
|
|
577
581
|
matches = [canonical for canonical in CANONICAL_ARGS if canonical.startswith(prefix)]
|
|
578
582
|
|
|
579
|
-
|
|
583
|
+
|
|
580
584
|
if len(matches) == 1 and matches[0] == arg:
|
|
581
585
|
arg_map[arg].append(prefix)
|
|
582
586
|
|
|
@@ -668,7 +672,7 @@ BASH_COMMANDS = [
|
|
|
668
672
|
"until",
|
|
669
673
|
"wait",
|
|
670
674
|
"while",
|
|
671
|
-
|
|
675
|
+
|
|
672
676
|
"ls",
|
|
673
677
|
"cp",
|
|
674
678
|
"mv",
|
|
@@ -765,26 +769,26 @@ def start_interactive_session(command: str) -> int:
|
|
|
765
769
|
Starts an interactive session. Only works on Unix. On Windows, print a message and return 1.
|
|
766
770
|
"""
|
|
767
771
|
ON_WINDOWS = platform.system().lower().startswith("win")
|
|
768
|
-
if ON_WINDOWS or termios is None or tty is None or pty is None or select is None or signal is None:
|
|
772
|
+
if ON_WINDOWS or termios is None or tty is None or pty is None or select is None or signal is None or tty is None:
|
|
769
773
|
print("Interactive terminal sessions are not supported on Windows.")
|
|
770
774
|
return 1
|
|
771
|
-
|
|
775
|
+
|
|
772
776
|
old_tty = termios.tcgetattr(sys.stdin)
|
|
773
777
|
try:
|
|
774
|
-
|
|
778
|
+
|
|
775
779
|
master_fd, slave_fd = pty.openpty()
|
|
776
780
|
|
|
777
|
-
|
|
781
|
+
|
|
778
782
|
p = subprocess.Popen(
|
|
779
783
|
command,
|
|
780
784
|
stdin=slave_fd,
|
|
781
785
|
stdout=slave_fd,
|
|
782
786
|
stderr=slave_fd,
|
|
783
787
|
shell=True,
|
|
784
|
-
preexec_fn=os.setsid,
|
|
788
|
+
preexec_fn=os.setsid,
|
|
785
789
|
)
|
|
786
790
|
|
|
787
|
-
|
|
791
|
+
|
|
788
792
|
tty.setraw(sys.stdin.fileno())
|
|
789
793
|
|
|
790
794
|
def handle_timeout(signum, frame):
|
|
@@ -802,9 +806,9 @@ def start_interactive_session(command: str) -> int:
|
|
|
802
806
|
else:
|
|
803
807
|
break
|
|
804
808
|
|
|
805
|
-
|
|
809
|
+
|
|
806
810
|
signal.signal(signal.SIGALRM, handle_timeout)
|
|
807
|
-
signal.alarm(5)
|
|
811
|
+
signal.alarm(5)
|
|
808
812
|
try:
|
|
809
813
|
p.wait()
|
|
810
814
|
except TimeoutError:
|
|
@@ -817,7 +821,7 @@ def start_interactive_session(command: str) -> int:
|
|
|
817
821
|
signal.alarm(0)
|
|
818
822
|
|
|
819
823
|
finally:
|
|
820
|
-
|
|
824
|
+
|
|
821
825
|
termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, old_tty)
|
|
822
826
|
|
|
823
827
|
return p.returncode
|
|
@@ -1009,21 +1013,21 @@ def validate_bash_command(command_parts: list) -> bool:
|
|
|
1009
1013
|
base_command = command_parts[0]
|
|
1010
1014
|
|
|
1011
1015
|
if base_command == 'which':
|
|
1012
|
-
return False
|
|
1016
|
+
return False
|
|
1013
1017
|
|
|
1014
1018
|
|
|
1015
|
-
|
|
1019
|
+
|
|
1016
1020
|
INTERACTIVE_COMMANDS = ["ipython", "python", "sqlite3", "r"]
|
|
1017
1021
|
TERMINAL_EDITORS = ["vim", "nano", "emacs"]
|
|
1018
1022
|
if base_command in TERMINAL_EDITORS or base_command in INTERACTIVE_COMMANDS:
|
|
1019
1023
|
return True
|
|
1020
1024
|
|
|
1021
1025
|
if base_command not in COMMAND_PATTERNS and base_command not in BASH_COMMANDS:
|
|
1022
|
-
return False
|
|
1026
|
+
return False
|
|
1023
1027
|
|
|
1024
1028
|
pattern = COMMAND_PATTERNS.get(base_command)
|
|
1025
1029
|
if not pattern:
|
|
1026
|
-
return True
|
|
1030
|
+
return True
|
|
1027
1031
|
|
|
1028
1032
|
args = []
|
|
1029
1033
|
flags = []
|
|
@@ -1033,14 +1037,14 @@ def validate_bash_command(command_parts: list) -> bool:
|
|
|
1033
1037
|
if part.startswith("-"):
|
|
1034
1038
|
flags.append(part)
|
|
1035
1039
|
if part not in pattern["flags"]:
|
|
1036
|
-
return False
|
|
1040
|
+
return False
|
|
1037
1041
|
else:
|
|
1038
1042
|
args.append(part)
|
|
1039
1043
|
|
|
1040
|
-
|
|
1044
|
+
|
|
1041
1045
|
if base_command == "who" and args:
|
|
1042
1046
|
return False
|
|
1043
|
-
|
|
1047
|
+
|
|
1044
1048
|
if pattern.get("requires_arg", False) and not args:
|
|
1045
1049
|
return False
|
|
1046
1050
|
|
|
@@ -1077,7 +1081,7 @@ def execute_set_command(command: str, value: str) -> str:
|
|
|
1077
1081
|
|
|
1078
1082
|
config_path = os.path.expanduser("~/.npcshrc")
|
|
1079
1083
|
|
|
1080
|
-
|
|
1084
|
+
|
|
1081
1085
|
var_map = {
|
|
1082
1086
|
"model": "NPCSH_CHAT_MODEL",
|
|
1083
1087
|
"provider": "NPCSH_CHAT_PROVIDER",
|
|
@@ -1089,14 +1093,14 @@ def execute_set_command(command: str, value: str) -> str:
|
|
|
1089
1093
|
|
|
1090
1094
|
env_var = var_map[command]
|
|
1091
1095
|
|
|
1092
|
-
|
|
1096
|
+
|
|
1093
1097
|
if os.path.exists(config_path):
|
|
1094
1098
|
with open(config_path, "r") as f:
|
|
1095
1099
|
lines = f.readlines()
|
|
1096
1100
|
else:
|
|
1097
1101
|
lines = []
|
|
1098
1102
|
|
|
1099
|
-
|
|
1103
|
+
|
|
1100
1104
|
property_exists = False
|
|
1101
1105
|
for i, line in enumerate(lines):
|
|
1102
1106
|
if line.startswith(f"export {env_var}="):
|
|
@@ -1107,7 +1111,7 @@ def execute_set_command(command: str, value: str) -> str:
|
|
|
1107
1111
|
if not property_exists:
|
|
1108
1112
|
lines.append(f"export {env_var}='{value}'\n")
|
|
1109
1113
|
|
|
1110
|
-
|
|
1114
|
+
|
|
1111
1115
|
with open(config_path, "w") as f:
|
|
1112
1116
|
f.writelines(lines)
|
|
1113
1117
|
|
|
@@ -1139,7 +1143,7 @@ def set_npcsh_initialized() -> None:
|
|
|
1139
1143
|
npcshrc.write(content)
|
|
1140
1144
|
npcshrc.truncate()
|
|
1141
1145
|
|
|
1142
|
-
|
|
1146
|
+
|
|
1143
1147
|
os.environ["NPCSH_INITIALIZED"] = "1"
|
|
1144
1148
|
print("NPCSH initialization flag set in .npcshrc")
|
|
1145
1149
|
|
|
@@ -1158,7 +1162,7 @@ def file_has_changed(source_path: str, destination_path: str) -> bool:
|
|
|
1158
1162
|
A boolean indicating whether the files are different
|
|
1159
1163
|
"""
|
|
1160
1164
|
|
|
1161
|
-
|
|
1165
|
+
|
|
1162
1166
|
return not filecmp.cmp(source_path, destination_path, shallow=False)
|
|
1163
1167
|
|
|
1164
1168
|
|
|
@@ -1245,7 +1249,7 @@ def read_rc_file_windows(path):
|
|
|
1245
1249
|
for line in f:
|
|
1246
1250
|
line = line.strip()
|
|
1247
1251
|
if line and not line.startswith("#"):
|
|
1248
|
-
|
|
1252
|
+
|
|
1249
1253
|
match = re.match(r'^([A-Z_]+)\s*=\s*[\'"](.*?)[\'"]$', line)
|
|
1250
1254
|
if match:
|
|
1251
1255
|
key, value = match.groups()
|
|
@@ -1254,11 +1258,11 @@ def read_rc_file_windows(path):
|
|
|
1254
1258
|
|
|
1255
1259
|
|
|
1256
1260
|
def get_setting_windows(key, default=None):
|
|
1257
|
-
|
|
1261
|
+
|
|
1258
1262
|
if env_value := os.getenv(key):
|
|
1259
1263
|
return env_value
|
|
1260
1264
|
|
|
1261
|
-
|
|
1265
|
+
|
|
1262
1266
|
config = read_rc_file_windows(get_npcshrc_path_windows())
|
|
1263
1267
|
return config.get(key, default)
|
|
1264
1268
|
|
|
@@ -1303,7 +1307,7 @@ READLINE_HISTORY_FILE = os.path.expanduser("~/.npcsh_readline_history")
|
|
|
1303
1307
|
DEFAULT_NPC_TEAM_PATH = os.path.expanduser("~/.npcsh/npc_team/")
|
|
1304
1308
|
PROJECT_NPC_TEAM_PATH = "./npc_team/"
|
|
1305
1309
|
|
|
1306
|
-
|
|
1310
|
+
|
|
1307
1311
|
try:
|
|
1308
1312
|
chroma_client = chromadb.PersistentClient(path=EMBEDDINGS_DB_PATH) if chromadb else None
|
|
1309
1313
|
except Exception as e:
|
|
@@ -1333,11 +1337,11 @@ def get_path_executables() -> List[str]:
|
|
|
1333
1337
|
|
|
1334
1338
|
import logging
|
|
1335
1339
|
|
|
1336
|
-
|
|
1340
|
+
|
|
1337
1341
|
completion_logger = logging.getLogger('npcsh.completion')
|
|
1338
|
-
completion_logger.setLevel(logging.WARNING)
|
|
1342
|
+
completion_logger.setLevel(logging.WARNING)
|
|
1343
|
+
|
|
1339
1344
|
|
|
1340
|
-
# Add handler if not already present
|
|
1341
1345
|
if not completion_logger.handlers:
|
|
1342
1346
|
handler = logging.StreamHandler(sys.stderr)
|
|
1343
1347
|
formatter = logging.Formatter('[%(name)s] %(message)s')
|
|
@@ -1356,7 +1360,7 @@ def make_completer(shell_state: ShellState, router: Any):
|
|
|
1356
1360
|
|
|
1357
1361
|
matches = []
|
|
1358
1362
|
|
|
1359
|
-
|
|
1363
|
+
|
|
1360
1364
|
if begidx > 0 and buffer[begidx-1] == '/':
|
|
1361
1365
|
completion_logger.debug(f"Slash command completion - text='{text}'")
|
|
1362
1366
|
slash_commands = get_slash_commands(shell_state, router)
|
|
@@ -1414,19 +1418,19 @@ def get_slash_commands(state: ShellState, router: Any) -> List[str]:
|
|
|
1414
1418
|
commands.extend(router_cmds)
|
|
1415
1419
|
completion_logger.debug(f"Router commands: {router_cmds}")
|
|
1416
1420
|
|
|
1417
|
-
|
|
1421
|
+
|
|
1418
1422
|
if state.team and hasattr(state.team, 'jinxs_dict'):
|
|
1419
1423
|
jinx_cmds = [f"/{jinx}" for jinx in state.team.jinxs_dict.keys()]
|
|
1420
1424
|
commands.extend(jinx_cmds)
|
|
1421
1425
|
completion_logger.debug(f"Jinx commands: {jinx_cmds}")
|
|
1422
1426
|
|
|
1423
|
-
|
|
1427
|
+
|
|
1424
1428
|
if state.team and hasattr(state.team, 'npcs'):
|
|
1425
1429
|
npc_cmds = [f"/{npc}" for npc in state.team.npcs.keys()]
|
|
1426
1430
|
commands.extend(npc_cmds)
|
|
1427
1431
|
completion_logger.debug(f"NPC commands: {npc_cmds}")
|
|
1428
1432
|
|
|
1429
|
-
|
|
1433
|
+
|
|
1430
1434
|
mode_cmds = ['/cmd', '/agent', '/chat']
|
|
1431
1435
|
commands.extend(mode_cmds)
|
|
1432
1436
|
completion_logger.debug(f"Mode commands: {mode_cmds}")
|
|
@@ -1460,7 +1464,7 @@ def get_file_completions(text: str) -> List[str]:
|
|
|
1460
1464
|
else:
|
|
1461
1465
|
completion = os.path.join(basedir, item)
|
|
1462
1466
|
|
|
1463
|
-
|
|
1467
|
+
|
|
1464
1468
|
matches.append(completion)
|
|
1465
1469
|
except (PermissionError, OSError):
|
|
1466
1470
|
pass
|
|
@@ -1470,15 +1474,15 @@ def get_file_completions(text: str) -> List[str]:
|
|
|
1470
1474
|
return []
|
|
1471
1475
|
def is_command_position(buffer: str, begidx: int) -> bool:
|
|
1472
1476
|
"""Determine if cursor is at a command position"""
|
|
1473
|
-
|
|
1477
|
+
|
|
1474
1478
|
before_word = buffer[:begidx]
|
|
1475
1479
|
|
|
1476
|
-
|
|
1480
|
+
|
|
1477
1481
|
parts = re.split(r'[|;&]', before_word)
|
|
1478
1482
|
current_command_part = parts[-1].strip()
|
|
1479
1483
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1484
|
+
|
|
1485
|
+
|
|
1482
1486
|
return len(current_command_part) == 0
|
|
1483
1487
|
|
|
1484
1488
|
|
|
@@ -1608,10 +1612,10 @@ def format_file_listing(output: str) -> str:
|
|
|
1608
1612
|
colored_filepath = colored(filepath_guess, color, attrs=attrs)
|
|
1609
1613
|
|
|
1610
1614
|
if len(parts) > 1 :
|
|
1611
|
-
|
|
1615
|
+
|
|
1612
1616
|
colored_line = " ".join(parts[:-1] + [colored_filepath])
|
|
1613
1617
|
else:
|
|
1614
|
-
|
|
1618
|
+
|
|
1615
1619
|
colored_line = colored_filepath
|
|
1616
1620
|
|
|
1617
1621
|
colored_lines.append(colored_line)
|
|
@@ -1627,7 +1631,7 @@ def wrap_text(text: str, width: int = 80) -> str:
|
|
|
1627
1631
|
lines.append(paragraph)
|
|
1628
1632
|
return "\n".join(lines)
|
|
1629
1633
|
|
|
1630
|
-
|
|
1634
|
+
|
|
1631
1635
|
|
|
1632
1636
|
def setup_readline() -> str:
|
|
1633
1637
|
"""Setup readline with history and completion"""
|
|
@@ -1635,7 +1639,7 @@ def setup_readline() -> str:
|
|
|
1635
1639
|
readline.read_history_file(READLINE_HISTORY_FILE)
|
|
1636
1640
|
readline.set_history_length(1000)
|
|
1637
1641
|
|
|
1638
|
-
|
|
1642
|
+
|
|
1639
1643
|
readline.parse_and_bind("tab: complete")
|
|
1640
1644
|
|
|
1641
1645
|
readline.parse_and_bind("set enable-bracketed-paste on")
|
|
@@ -1666,7 +1670,7 @@ def store_command_embeddings(command: str, output: Any, state: ShellState):
|
|
|
1666
1670
|
|
|
1667
1671
|
try:
|
|
1668
1672
|
output_str = str(output) if output else ""
|
|
1669
|
-
if not command and not output_str: return
|
|
1673
|
+
if not command and not output_str: return
|
|
1670
1674
|
|
|
1671
1675
|
texts_to_embed = [command, output_str]
|
|
1672
1676
|
|
|
@@ -1716,7 +1720,7 @@ def handle_interactive_command(cmd_parts: List[str], state: ShellState) -> Tuple
|
|
|
1716
1720
|
command_name = cmd_parts[0]
|
|
1717
1721
|
print(f"Starting interactive {command_name} session...")
|
|
1718
1722
|
try:
|
|
1719
|
-
|
|
1723
|
+
|
|
1720
1724
|
full_command_str = " ".join(cmd_parts)
|
|
1721
1725
|
return_code = start_interactive_session(full_command_str)
|
|
1722
1726
|
output = f"Interactive {command_name} session ended with return code {return_code}"
|
|
@@ -1735,7 +1739,7 @@ def handle_cd_command(cmd_parts: List[str], state: ShellState) -> Tuple[ShellSta
|
|
|
1735
1739
|
output = colored(f"cd: no such file or directory: {target_path}", "red")
|
|
1736
1740
|
except Exception as e:
|
|
1737
1741
|
output = colored(f"cd: error changing directory: {e}", "red")
|
|
1738
|
-
os.chdir(original_path)
|
|
1742
|
+
os.chdir(original_path)
|
|
1739
1743
|
|
|
1740
1744
|
return state, output
|
|
1741
1745
|
|
|
@@ -1806,21 +1810,21 @@ def parse_generic_command_flags(parts: List[str]) -> Tuple[Dict[str, Any], List[
|
|
|
1806
1810
|
key, value = key_part.split('=', 1)
|
|
1807
1811
|
parsed_kwargs[key] = _try_convert_type(value)
|
|
1808
1812
|
else:
|
|
1809
|
-
|
|
1813
|
+
|
|
1810
1814
|
if i + 1 < len(parts) and not parts[i + 1].startswith('-'):
|
|
1811
1815
|
parsed_kwargs[key_part] = _try_convert_type(parts[i + 1])
|
|
1812
|
-
i += 1
|
|
1816
|
+
i += 1
|
|
1813
1817
|
else:
|
|
1814
|
-
parsed_kwargs[key_part] = True
|
|
1818
|
+
parsed_kwargs[key_part] = True
|
|
1815
1819
|
|
|
1816
1820
|
elif part.startswith('-'):
|
|
1817
1821
|
key = part[1:]
|
|
1818
|
-
|
|
1822
|
+
|
|
1819
1823
|
if i + 1 < len(parts) and not parts[i + 1].startswith('-'):
|
|
1820
1824
|
parsed_kwargs[key] = _try_convert_type(parts[i + 1])
|
|
1821
|
-
i += 1
|
|
1825
|
+
i += 1
|
|
1822
1826
|
else:
|
|
1823
|
-
parsed_kwargs[key] = True
|
|
1827
|
+
parsed_kwargs[key] = True
|
|
1824
1828
|
|
|
1825
1829
|
elif '=' in part and not part.startswith('-'):
|
|
1826
1830
|
key, value = part.split('=', 1)
|
|
@@ -1837,7 +1841,7 @@ def parse_generic_command_flags(parts: List[str]) -> Tuple[Dict[str, Any], List[
|
|
|
1837
1841
|
def should_skip_kg_processing(user_input: str, assistant_output: str) -> bool:
|
|
1838
1842
|
"""Determine if this interaction is too trivial for KG processing"""
|
|
1839
1843
|
|
|
1840
|
-
|
|
1844
|
+
|
|
1841
1845
|
if len(user_input.strip()) < 10:
|
|
1842
1846
|
return True
|
|
1843
1847
|
|
|
@@ -1866,7 +1870,7 @@ def execute_slash_command(command: str,
|
|
|
1866
1870
|
all_command_parts = shlex.split(command)
|
|
1867
1871
|
command_name = all_command_parts[0].lstrip('/')
|
|
1868
1872
|
|
|
1869
|
-
|
|
1873
|
+
|
|
1870
1874
|
if command_name in ['n', 'npc']:
|
|
1871
1875
|
npc_to_switch_to = all_command_parts[1] if len(all_command_parts) > 1 else None
|
|
1872
1876
|
if npc_to_switch_to and state.team and npc_to_switch_to in state.team.npcs:
|
|
@@ -1876,7 +1880,7 @@ def execute_slash_command(command: str,
|
|
|
1876
1880
|
available_npcs = list(state.team.npcs.keys()) if state.team else []
|
|
1877
1881
|
return state, colored(f"NPC '{npc_to_switch_to}' not found. Available NPCs: {', '.join(available_npcs)}", "red")
|
|
1878
1882
|
|
|
1879
|
-
|
|
1883
|
+
|
|
1880
1884
|
handler = router.get_route(command_name)
|
|
1881
1885
|
if handler:
|
|
1882
1886
|
parsed_flags, positional_args = parse_generic_command_flags(all_command_parts[1:])
|
|
@@ -1892,12 +1896,12 @@ def execute_slash_command(command: str,
|
|
|
1892
1896
|
'positional_args': positional_args,
|
|
1893
1897
|
'plonk_context': state.team.shared_context.get('PLONK_CONTEXT') if state.team and hasattr(state.team, 'shared_context') else None,
|
|
1894
1898
|
|
|
1895
|
-
|
|
1899
|
+
|
|
1896
1900
|
'model': state.npc.model if isinstance(state.npc, NPC) and state.npc.model else state.chat_model,
|
|
1897
1901
|
'provider': state.npc.provider if isinstance(state.npc, NPC) and state.npc.provider else state.chat_provider,
|
|
1898
1902
|
'npc': state.npc,
|
|
1899
1903
|
|
|
1900
|
-
|
|
1904
|
+
|
|
1901
1905
|
'sprovider': state.search_provider,
|
|
1902
1906
|
'emodel': state.embedding_model,
|
|
1903
1907
|
'eprovider': state.embedding_provider,
|
|
@@ -1918,7 +1922,7 @@ def execute_slash_command(command: str,
|
|
|
1918
1922
|
|
|
1919
1923
|
render_markdown(f'- Calling {command_name} handler {kwarg_part} ')
|
|
1920
1924
|
|
|
1921
|
-
|
|
1925
|
+
|
|
1922
1926
|
if 'model' in normalized_flags and 'provider' not in normalized_flags:
|
|
1923
1927
|
inferred_provider = lookup_provider(normalized_flags['model'])
|
|
1924
1928
|
if inferred_provider:
|
|
@@ -1947,7 +1951,7 @@ def execute_slash_command(command: str,
|
|
|
1947
1951
|
traceback.print_exc()
|
|
1948
1952
|
return state, colored(f"Error executing slash command '{command_name}': {e}", "red")
|
|
1949
1953
|
|
|
1950
|
-
|
|
1954
|
+
|
|
1951
1955
|
active_npc = state.npc if isinstance(state.npc, NPC) else None
|
|
1952
1956
|
jinx_to_execute = None
|
|
1953
1957
|
executor = None
|
|
@@ -1959,16 +1963,16 @@ def execute_slash_command(command: str,
|
|
|
1959
1963
|
jinx_to_execute = state.team.jinxs_dict[command_name]
|
|
1960
1964
|
executor = state.team
|
|
1961
1965
|
if jinx_to_execute:
|
|
1962
|
-
args = all_command_parts[1:]
|
|
1966
|
+
args = all_command_parts[1:]
|
|
1963
1967
|
try:
|
|
1964
|
-
|
|
1968
|
+
|
|
1965
1969
|
input_values = {}
|
|
1966
1970
|
if hasattr(jinx_to_execute, 'inputs') and jinx_to_execute.inputs:
|
|
1967
1971
|
for i, input_name in enumerate(jinx_to_execute.inputs):
|
|
1968
1972
|
if i < len(args):
|
|
1969
1973
|
input_values[input_name] = args[i]
|
|
1970
1974
|
|
|
1971
|
-
|
|
1975
|
+
|
|
1972
1976
|
if isinstance(executor, NPC):
|
|
1973
1977
|
jinx_output = jinx_to_execute.execute(
|
|
1974
1978
|
input_values=input_values,
|
|
@@ -1976,7 +1980,7 @@ def execute_slash_command(command: str,
|
|
|
1976
1980
|
npc=executor,
|
|
1977
1981
|
messages=state.messages
|
|
1978
1982
|
)
|
|
1979
|
-
else:
|
|
1983
|
+
else:
|
|
1980
1984
|
jinx_output = jinx_to_execute.execute(
|
|
1981
1985
|
input_values=input_values,
|
|
1982
1986
|
jinxs_dict=executor.jinxs_dict if hasattr(executor, 'jinxs_dict') else {},
|
|
@@ -2097,7 +2101,7 @@ def process_pipeline_command(
|
|
|
2097
2101
|
stream=stream_final,
|
|
2098
2102
|
context=info,
|
|
2099
2103
|
)
|
|
2100
|
-
|
|
2104
|
+
|
|
2101
2105
|
|
|
2102
2106
|
if not review:
|
|
2103
2107
|
if isinstance(llm_result, dict):
|
|
@@ -2131,7 +2135,7 @@ def review_and_iterate_command(
|
|
|
2131
2135
|
Simple iteration on LLM command result to improve quality.
|
|
2132
2136
|
"""
|
|
2133
2137
|
|
|
2134
|
-
|
|
2138
|
+
|
|
2135
2139
|
if isinstance(initial_result, dict):
|
|
2136
2140
|
current_output = initial_result.get("output")
|
|
2137
2141
|
current_messages = initial_result.get("messages", state.messages)
|
|
@@ -2139,7 +2143,7 @@ def review_and_iterate_command(
|
|
|
2139
2143
|
current_output = initial_result
|
|
2140
2144
|
current_messages = state.messages
|
|
2141
2145
|
|
|
2142
|
-
|
|
2146
|
+
|
|
2143
2147
|
refinement_prompt = f"""
|
|
2144
2148
|
The previous response to "{original_command}" was:
|
|
2145
2149
|
{current_output}
|
|
@@ -2147,7 +2151,7 @@ The previous response to "{original_command}" was:
|
|
|
2147
2151
|
Please review and improve this response if needed. Provide a better, more complete answer.
|
|
2148
2152
|
"""
|
|
2149
2153
|
|
|
2150
|
-
|
|
2154
|
+
|
|
2151
2155
|
refined_result = check_llm_command(
|
|
2152
2156
|
refinement_prompt,
|
|
2153
2157
|
model=exec_model,
|
|
@@ -2162,7 +2166,7 @@ Please review and improve this response if needed. Provide a better, more comple
|
|
|
2162
2166
|
context=info,
|
|
2163
2167
|
)
|
|
2164
2168
|
|
|
2165
|
-
|
|
2169
|
+
|
|
2166
2170
|
if isinstance(refined_result, dict):
|
|
2167
2171
|
state.messages = refined_result.get("messages", current_messages)
|
|
2168
2172
|
return state, refined_result.get("output", current_output)
|
|
@@ -2198,8 +2202,8 @@ def execute_command(
|
|
|
2198
2202
|
active_model = npc_model or state.chat_model
|
|
2199
2203
|
active_provider = npc_provider or state.chat_provider
|
|
2200
2204
|
if state.current_mode == 'agent':
|
|
2201
|
-
|
|
2202
|
-
|
|
2205
|
+
|
|
2206
|
+
|
|
2203
2207
|
for i, cmd_segment in enumerate(commands):
|
|
2204
2208
|
render_markdown(f'- Executing command {i+1}/{len(commands)}')
|
|
2205
2209
|
is_last_command = (i == len(commands) - 1)
|
|
@@ -2234,7 +2238,7 @@ def execute_command(
|
|
|
2234
2238
|
except Exception:
|
|
2235
2239
|
print(f"Warning: Cannot convert output to string for piping: {type(output)}", file=sys.stderr)
|
|
2236
2240
|
stdin_for_next = None
|
|
2237
|
-
else:
|
|
2241
|
+
else:
|
|
2238
2242
|
stdin_for_next = None
|
|
2239
2243
|
except Exception as pipeline_error:
|
|
2240
2244
|
import traceback
|
|
@@ -2249,7 +2253,7 @@ def execute_command(
|
|
|
2249
2253
|
|
|
2250
2254
|
|
|
2251
2255
|
elif state.current_mode == 'chat':
|
|
2252
|
-
|
|
2256
|
+
|
|
2253
2257
|
cmd_parts = parse_command_safely(command)
|
|
2254
2258
|
is_probably_bash = (
|
|
2255
2259
|
cmd_parts
|
|
@@ -2274,9 +2278,9 @@ def execute_command(
|
|
|
2274
2278
|
except Exception as bash_err:
|
|
2275
2279
|
return state, colored(f"Bash execution failed: {bash_err}", "red")
|
|
2276
2280
|
except Exception:
|
|
2277
|
-
pass
|
|
2281
|
+
pass
|
|
2278
2282
|
|
|
2279
|
-
|
|
2283
|
+
|
|
2280
2284
|
response = get_llm_response(
|
|
2281
2285
|
command,
|
|
2282
2286
|
model=active_model,
|
|
@@ -2390,7 +2394,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
2390
2394
|
print(f"Warning: Could not load context file {filename}: {e}")
|
|
2391
2395
|
|
|
2392
2396
|
forenpc_name = team_ctx.get("forenpc", default_forenpc_name)
|
|
2393
|
-
|
|
2397
|
+
|
|
2394
2398
|
|
|
2395
2399
|
if team_ctx.get("use_global_jinxs", False):
|
|
2396
2400
|
jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
|
|
@@ -2404,7 +2408,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
2404
2408
|
forenpc_path = os.path.join(team_dir, f"{forenpc_name}.npc")
|
|
2405
2409
|
|
|
2406
2410
|
|
|
2407
|
-
|
|
2411
|
+
|
|
2408
2412
|
|
|
2409
2413
|
|
|
2410
2414
|
|
|
@@ -2430,7 +2434,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
2430
2434
|
if not npc_obj.provider:
|
|
2431
2435
|
npc_obj.provider = initial_state.chat_provider
|
|
2432
2436
|
|
|
2433
|
-
|
|
2437
|
+
|
|
2434
2438
|
if team.forenpc and isinstance(team.forenpc, NPC):
|
|
2435
2439
|
if not team.forenpc.model:
|
|
2436
2440
|
team.forenpc.model = initial_state.chat_model
|
|
@@ -2442,7 +2446,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
2442
2446
|
elif team_dir and os.path.basename(team_dir) != 'npc_team':
|
|
2443
2447
|
team.name = os.path.basename(team_dir)
|
|
2444
2448
|
else:
|
|
2445
|
-
team.name = "global_team"
|
|
2449
|
+
team.name = "global_team"
|
|
2446
2450
|
|
|
2447
2451
|
return command_history, team, forenpc_obj
|
|
2448
2452
|
|
|
@@ -2460,7 +2464,7 @@ def process_result(
|
|
|
2460
2464
|
team_name = result_state.team.name if result_state.team else "__none__"
|
|
2461
2465
|
npc_name = result_state.npc.name if isinstance(result_state.npc, NPC) else "__none__"
|
|
2462
2466
|
|
|
2463
|
-
|
|
2467
|
+
|
|
2464
2468
|
active_npc = result_state.npc if isinstance(result_state.npc, NPC) else NPC(
|
|
2465
2469
|
name="default",
|
|
2466
2470
|
model=result_state.chat_model,
|
|
@@ -2543,7 +2547,7 @@ def process_result(
|
|
|
2543
2547
|
except Exception as e:
|
|
2544
2548
|
print(colored(f"Error during real-time KG evolution: {e}", "red"))
|
|
2545
2549
|
|
|
2546
|
-
|
|
2550
|
+
|
|
2547
2551
|
result_state.turn_count += 1
|
|
2548
2552
|
|
|
2549
2553
|
if result_state.turn_count > 0 and result_state.turn_count % 10 == 0:
|