npcsh 1.1.14__py3-none-any.whl → 1.1.16__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 +533 -80
- npcsh/mcp_server.py +2 -1
- npcsh/npc.py +84 -32
- npcsh/npc_team/alicanto.npc +22 -1
- npcsh/npc_team/corca.npc +28 -9
- npcsh/npc_team/frederic.npc +25 -4
- npcsh/npc_team/guac.npc +22 -0
- npcsh/npc_team/jinxs/bin/nql.jinx +141 -0
- npcsh/npc_team/jinxs/bin/sync.jinx +230 -0
- {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/bin}/vixynt.jinx +8 -30
- npcsh/npc_team/jinxs/bin/wander.jinx +152 -0
- npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +220 -0
- npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +40 -0
- npcsh/npc_team/jinxs/lib/browser/close_browser.jinx +14 -0
- npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +43 -0
- npcsh/npc_team/jinxs/lib/computer_use/click.jinx +23 -0
- npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +26 -0
- npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +37 -0
- npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +23 -0
- npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +27 -0
- npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +21 -0
- {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/edit_file.jinx +3 -3
- {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/load_file.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/paste.jinx +134 -0
- {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/search.jinx +2 -1
- npcsh/npc_team/jinxs/{code → lib/core}/sh.jinx +2 -8
- npcsh/npc_team/jinxs/{code → lib/core}/sql.jinx +1 -1
- npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +232 -0
- npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +184 -0
- npcsh/npc_team/jinxs/lib/research/arxiv.jinx +76 -0
- npcsh/npc_team/jinxs/lib/research/paper_search.jinx +101 -0
- npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +69 -0
- npcsh/npc_team/jinxs/{utils/core → lib/utils}/build.jinx +8 -8
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +176 -0
- npcsh/npc_team/jinxs/lib/utils/shh.jinx +17 -0
- npcsh/npc_team/jinxs/lib/utils/switch.jinx +62 -0
- npcsh/npc_team/jinxs/lib/utils/switches.jinx +61 -0
- npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +205 -0
- npcsh/npc_team/jinxs/lib/utils/verbose.jinx +17 -0
- npcsh/npc_team/kadiefa.npc +19 -1
- npcsh/npc_team/plonk.npc +26 -1
- npcsh/npc_team/plonkjr.npc +22 -1
- npcsh/npc_team/sibiji.npc +23 -2
- npcsh/npcsh.py +153 -39
- npcsh/ui.py +22 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/alicanto.npc +23 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/arxiv.jinx +76 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/browser_action.jinx +220 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/browser_screenshot.jinx +40 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/build.jinx +8 -8
- npcsh-1.1.16.data/data/npcsh/npc_team/click.jinx +23 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/close_browser.jinx +14 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/convene.jinx +232 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/corca.npc +31 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/delegate.jinx +184 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/edit_file.jinx +3 -3
- npcsh-1.1.16.data/data/npcsh/npc_team/frederic.npc +27 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/guac.npc +22 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/jinxs.jinx +176 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/kadiefa.npc +21 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/key_press.jinx +26 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/launch_app.jinx +37 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/load_file.jinx +1 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/nql.jinx +141 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/open_browser.jinx +43 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/paper_search.jinx +101 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/paste.jinx +134 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/plonk.npc +27 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/plonkjr.npc +23 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/screenshot.jinx +23 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/search.jinx +2 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/semantic_scholar.jinx +69 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sh.jinx +2 -8
- npcsh-1.1.16.data/data/npcsh/npc_team/shh.jinx +17 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/sibiji.npc +24 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sql.jinx +1 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/switch.jinx +62 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/switches.jinx +61 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/sync.jinx +230 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/teamviz.jinx +205 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/type_text.jinx +27 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/verbose.jinx +17 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/vixynt.jinx +8 -30
- npcsh-1.1.16.data/data/npcsh/npc_team/wait.jinx +21 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +152 -0
- {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/METADATA +399 -58
- npcsh-1.1.16.dist-info/RECORD +170 -0
- npcsh-1.1.16.dist-info/entry_points.txt +19 -0
- npcsh-1.1.16.dist-info/top_level.txt +2 -0
- project/__init__.py +1 -0
- npcsh/npc_team/foreman.npc +0 -7
- npcsh/npc_team/jinxs/modes/alicanto.jinx +0 -194
- npcsh/npc_team/jinxs/modes/corca.jinx +0 -249
- npcsh/npc_team/jinxs/modes/guac.jinx +0 -317
- npcsh/npc_team/jinxs/modes/plonk.jinx +0 -214
- npcsh/npc_team/jinxs/modes/pti.jinx +0 -170
- npcsh/npc_team/jinxs/modes/wander.jinx +0 -186
- npcsh/npc_team/jinxs/utils/agent.jinx +0 -17
- npcsh/npc_team/jinxs/utils/core/jinxs.jinx +0 -32
- npcsh-1.1.14.data/data/npcsh/npc_team/agent.jinx +0 -17
- npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.jinx +0 -194
- npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.npc +0 -2
- npcsh-1.1.14.data/data/npcsh/npc_team/corca.jinx +0 -249
- npcsh-1.1.14.data/data/npcsh/npc_team/corca.npc +0 -12
- npcsh-1.1.14.data/data/npcsh/npc_team/foreman.npc +0 -7
- npcsh-1.1.14.data/data/npcsh/npc_team/frederic.npc +0 -6
- npcsh-1.1.14.data/data/npcsh/npc_team/guac.jinx +0 -317
- npcsh-1.1.14.data/data/npcsh/npc_team/jinxs.jinx +0 -32
- npcsh-1.1.14.data/data/npcsh/npc_team/kadiefa.npc +0 -3
- npcsh-1.1.14.data/data/npcsh/npc_team/plonk.jinx +0 -214
- npcsh-1.1.14.data/data/npcsh/npc_team/plonk.npc +0 -2
- npcsh-1.1.14.data/data/npcsh/npc_team/plonkjr.npc +0 -2
- npcsh-1.1.14.data/data/npcsh/npc_team/pti.jinx +0 -170
- npcsh-1.1.14.data/data/npcsh/npc_team/sibiji.npc +0 -3
- npcsh-1.1.14.data/data/npcsh/npc_team/wander.jinx +0 -186
- npcsh-1.1.14.dist-info/RECORD +0 -135
- npcsh-1.1.14.dist-info/entry_points.txt +0 -9
- npcsh-1.1.14.dist-info/top_level.txt +0 -1
- /npcsh/npc_team/jinxs/{utils → bin}/roll.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → bin}/sample.jinx +0 -0
- /npcsh/npc_team/jinxs/{modes → bin}/spool.jinx +0 -0
- /npcsh/npc_team/jinxs/{modes → bin}/yap.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/computer_use}/trigger.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/chat.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/cmd.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/compress.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/ots.jinx +0 -0
- /npcsh/npc_team/jinxs/{code → lib/core}/python.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/sleep.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils/core → lib/utils}/compile.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils/core → lib/utils}/help.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils/core → lib/utils}/init.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/utils}/serve.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils/core → lib/utils}/set.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/utils}/usage.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/yap.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/WHEEL +0 -0
- {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/licenses/LICENSE +0 -0
npcsh/_state.py
CHANGED
|
@@ -21,20 +21,19 @@ import textwrap
|
|
|
21
21
|
from typing import Dict, List, Any, Tuple, Union, Optional, Callable
|
|
22
22
|
import yaml
|
|
23
23
|
|
|
24
|
-
# Setup
|
|
25
|
-
def
|
|
26
|
-
if os.environ.get("NPCSH_DEBUG", "0") == "1"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
logging.getLogger("npcsh.state").debug("Debug logging enabled via NPCSH_DEBUG=1")
|
|
24
|
+
# Setup logging - INFO by default, DEBUG if NPCSH_DEBUG=1
|
|
25
|
+
def _setup_logging():
|
|
26
|
+
level = logging.DEBUG if os.environ.get("NPCSH_DEBUG", "0") == "1" else logging.INFO
|
|
27
|
+
logging.basicConfig(
|
|
28
|
+
level=level,
|
|
29
|
+
format='%(message)s', # Simple format - just the message
|
|
30
|
+
datefmt='%H:%M:%S'
|
|
31
|
+
)
|
|
32
|
+
# Always show tool calls from llm_funcs at INFO level
|
|
33
|
+
logging.getLogger("npcpy.llm_funcs").setLevel(level)
|
|
34
|
+
logging.getLogger("npcsh.state").setLevel(level)
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
_setup_logging()
|
|
38
37
|
|
|
39
38
|
# Platform-specific imports
|
|
40
39
|
try:
|
|
@@ -167,6 +166,10 @@ class ShellState:
|
|
|
167
166
|
session_input_tokens: int = 0
|
|
168
167
|
session_output_tokens: int = 0
|
|
169
168
|
session_cost_usd: float = 0.0
|
|
169
|
+
# Session timing
|
|
170
|
+
session_start_time: float = field(default_factory=lambda: __import__('time').time())
|
|
171
|
+
# Logging level: "silent", "normal", "verbose"
|
|
172
|
+
log_level: str = "normal"
|
|
170
173
|
|
|
171
174
|
def get_model_for_command(self, model_type: str = "chat"):
|
|
172
175
|
if model_type == "chat":
|
|
@@ -182,7 +185,34 @@ class ShellState:
|
|
|
182
185
|
elif model_type == "video_gen":
|
|
183
186
|
return self.video_gen_model, self.video_gen_provider
|
|
184
187
|
else:
|
|
185
|
-
return self.chat_model, self.chat_provider
|
|
188
|
+
return self.chat_model, self.chat_provider
|
|
189
|
+
|
|
190
|
+
def set_log_level(self, level: str) -> str:
|
|
191
|
+
"""Set the logging level and configure npcpy loggers accordingly."""
|
|
192
|
+
level = level.lower()
|
|
193
|
+
if level not in ("silent", "normal", "verbose"):
|
|
194
|
+
return f"Invalid log level: {level}. Use 'silent', 'normal', or 'verbose'."
|
|
195
|
+
|
|
196
|
+
self.log_level = level
|
|
197
|
+
|
|
198
|
+
# Map to Python logging levels
|
|
199
|
+
level_map = {
|
|
200
|
+
"silent": logging.WARNING,
|
|
201
|
+
"normal": logging.INFO,
|
|
202
|
+
"verbose": logging.DEBUG,
|
|
203
|
+
}
|
|
204
|
+
log_level = level_map[level]
|
|
205
|
+
|
|
206
|
+
# Configure npcpy loggers
|
|
207
|
+
for logger_name in ["npcpy", "npcpy.gen", "npcpy.gen.response", "npcsh"]:
|
|
208
|
+
logger = logging.getLogger(logger_name)
|
|
209
|
+
logger.setLevel(log_level)
|
|
210
|
+
|
|
211
|
+
# Also set root logger for npcpy
|
|
212
|
+
logging.getLogger("npcpy").setLevel(log_level)
|
|
213
|
+
|
|
214
|
+
return f"Log level set to: {level}"
|
|
215
|
+
|
|
186
216
|
CONFIG_KEY_MAP = {
|
|
187
217
|
|
|
188
218
|
"model": "NPCSH_CHAT_MODEL",
|
|
@@ -338,10 +368,27 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
338
368
|
"""
|
|
339
369
|
)
|
|
340
370
|
|
|
341
|
-
# Package directories
|
|
342
|
-
package_dir =
|
|
371
|
+
# Package directories - use helper that handles PyInstaller bundles
|
|
372
|
+
package_dir = get_package_dir()
|
|
343
373
|
package_npc_team_dir = os.path.join(package_dir, "npc_team")
|
|
344
374
|
|
|
375
|
+
# Debug logging for package path resolution
|
|
376
|
+
if os.environ.get("NPCSH_DEBUG", "0") == "1":
|
|
377
|
+
print(f"[DEBUG] Package dir: {package_dir}")
|
|
378
|
+
print(f"[DEBUG] Package npc_team dir: {package_npc_team_dir}")
|
|
379
|
+
print(f"[DEBUG] npc_team exists: {os.path.exists(package_npc_team_dir)}")
|
|
380
|
+
if os.path.exists(package_npc_team_dir):
|
|
381
|
+
print(f"[DEBUG] npc_team contents: {os.listdir(package_npc_team_dir)}")
|
|
382
|
+
|
|
383
|
+
if not os.path.exists(package_npc_team_dir):
|
|
384
|
+
print(f"Warning: Package npc_team directory not found at {package_npc_team_dir}")
|
|
385
|
+
# For bundled executables, try to find it
|
|
386
|
+
if getattr(sys, 'frozen', False):
|
|
387
|
+
print(f"Running as frozen executable, _MEIPASS: {getattr(sys, '_MEIPASS', 'N/A')}")
|
|
388
|
+
if hasattr(sys, '_MEIPASS'):
|
|
389
|
+
print(f"Contents of _MEIPASS: {os.listdir(sys._MEIPASS)}")
|
|
390
|
+
return
|
|
391
|
+
|
|
345
392
|
user_npc_team_dir = os.path.expanduser("~/.npcsh/npc_team")
|
|
346
393
|
|
|
347
394
|
user_jinxs_dir = os.path.join(user_npc_team_dir, "jinxs")
|
|
@@ -369,31 +416,76 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
369
416
|
shutil.copy2(source_path, destination_path)
|
|
370
417
|
print(f"Copied ctx {filename} to {destination_path}")
|
|
371
418
|
|
|
372
|
-
# Copy jinxs directory RECURSIVELY
|
|
419
|
+
# Copy jinxs directory RECURSIVELY with manifest tracking
|
|
420
|
+
# This ensures we only sync package jinxs and can clean up old ones
|
|
373
421
|
package_jinxs_dir = os.path.join(package_npc_team_dir, "jinxs")
|
|
422
|
+
manifest_path = os.path.join(user_jinxs_dir, ".package_manifest.json")
|
|
423
|
+
|
|
424
|
+
# Load existing manifest of package-synced jinxs
|
|
425
|
+
old_package_jinxs = set()
|
|
426
|
+
if os.path.exists(manifest_path):
|
|
427
|
+
try:
|
|
428
|
+
import json
|
|
429
|
+
with open(manifest_path, 'r') as f:
|
|
430
|
+
old_package_jinxs = set(json.load(f).get('jinxs', []))
|
|
431
|
+
except:
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
# Track current package jinxs
|
|
435
|
+
current_package_jinxs = set()
|
|
436
|
+
|
|
374
437
|
if os.path.exists(package_jinxs_dir):
|
|
375
438
|
for root, dirs, files in os.walk(package_jinxs_dir):
|
|
376
439
|
# Calculate relative path from package_jinxs_dir
|
|
377
440
|
rel_path = os.path.relpath(root, package_jinxs_dir)
|
|
378
|
-
|
|
441
|
+
|
|
379
442
|
# Create corresponding directory in user jinxs
|
|
380
443
|
if rel_path == '.':
|
|
381
444
|
dest_dir = user_jinxs_dir
|
|
382
445
|
else:
|
|
383
446
|
dest_dir = os.path.join(user_jinxs_dir, rel_path)
|
|
384
447
|
os.makedirs(dest_dir, exist_ok=True)
|
|
385
|
-
|
|
448
|
+
|
|
386
449
|
# Copy all .jinx files in this directory
|
|
387
450
|
for filename in files:
|
|
388
451
|
if filename.endswith(".jinx"):
|
|
389
452
|
source_jinx_path = os.path.join(root, filename)
|
|
390
453
|
destination_jinx_path = os.path.join(dest_dir, filename)
|
|
391
|
-
|
|
454
|
+
jinx_rel_path = os.path.join(rel_path, filename) if rel_path != '.' else filename
|
|
455
|
+
current_package_jinxs.add(jinx_rel_path)
|
|
456
|
+
|
|
392
457
|
if not os.path.exists(destination_jinx_path) or file_has_changed(
|
|
393
458
|
source_jinx_path, destination_jinx_path
|
|
394
459
|
):
|
|
395
460
|
shutil.copy2(source_jinx_path, destination_jinx_path)
|
|
396
|
-
print(f"Copied jinx {
|
|
461
|
+
print(f"Copied jinx {jinx_rel_path} to {destination_jinx_path}")
|
|
462
|
+
|
|
463
|
+
# Clean up old package jinxs that are no longer in the package
|
|
464
|
+
# (but preserve user-created jinxs that were never in the manifest)
|
|
465
|
+
stale_jinxs = old_package_jinxs - current_package_jinxs
|
|
466
|
+
for stale_jinx in stale_jinxs:
|
|
467
|
+
stale_path = os.path.join(user_jinxs_dir, stale_jinx)
|
|
468
|
+
if os.path.exists(stale_path):
|
|
469
|
+
try:
|
|
470
|
+
os.remove(stale_path)
|
|
471
|
+
print(f"Removed stale package jinx: {stale_jinx}")
|
|
472
|
+
# Remove empty parent directories
|
|
473
|
+
parent_dir = os.path.dirname(stale_path)
|
|
474
|
+
while parent_dir != user_jinxs_dir:
|
|
475
|
+
if os.path.isdir(parent_dir) and not os.listdir(parent_dir):
|
|
476
|
+
os.rmdir(parent_dir)
|
|
477
|
+
print(f"Removed empty directory: {parent_dir}")
|
|
478
|
+
parent_dir = os.path.dirname(parent_dir)
|
|
479
|
+
except Exception as e:
|
|
480
|
+
print(f"Could not remove stale jinx {stale_jinx}: {e}")
|
|
481
|
+
|
|
482
|
+
# Save updated manifest
|
|
483
|
+
try:
|
|
484
|
+
import json
|
|
485
|
+
with open(manifest_path, 'w') as f:
|
|
486
|
+
json.dump({'jinxs': list(current_package_jinxs), 'updated': str(__import__('datetime').datetime.now())}, f, indent=2)
|
|
487
|
+
except Exception as e:
|
|
488
|
+
print(f"Could not save jinx manifest: {e}")
|
|
397
489
|
|
|
398
490
|
# Copy templates directory
|
|
399
491
|
templates = os.path.join(package_npc_team_dir, "templates")
|
|
@@ -1046,6 +1138,31 @@ def set_npcsh_initialized() -> None:
|
|
|
1046
1138
|
|
|
1047
1139
|
|
|
1048
1140
|
|
|
1141
|
+
def get_package_dir() -> str:
|
|
1142
|
+
"""
|
|
1143
|
+
Get the package directory, handling both normal Python and PyInstaller executables.
|
|
1144
|
+
|
|
1145
|
+
For normal Python: returns os.path.dirname(__file__)
|
|
1146
|
+
For PyInstaller: returns the bundled data directory (sys._MEIPASS/npcsh)
|
|
1147
|
+
"""
|
|
1148
|
+
# Check if running as a PyInstaller bundle
|
|
1149
|
+
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
|
1150
|
+
# Running as PyInstaller bundle - look for npcsh folder in _MEIPASS
|
|
1151
|
+
meipass = sys._MEIPASS
|
|
1152
|
+
# The package data should be at _MEIPASS/npcsh (based on PyInstaller config)
|
|
1153
|
+
bundled_path = os.path.join(meipass, 'npcsh')
|
|
1154
|
+
if os.path.exists(bundled_path):
|
|
1155
|
+
return bundled_path
|
|
1156
|
+
# Fallback: check if npc_team is directly in _MEIPASS
|
|
1157
|
+
if os.path.exists(os.path.join(meipass, 'npc_team')):
|
|
1158
|
+
return meipass
|
|
1159
|
+
# Last resort: return meipass and let caller handle
|
|
1160
|
+
return meipass
|
|
1161
|
+
else:
|
|
1162
|
+
# Normal Python execution
|
|
1163
|
+
return os.path.dirname(__file__)
|
|
1164
|
+
|
|
1165
|
+
|
|
1049
1166
|
def file_has_changed(source_path: str, destination_path: str) -> bool:
|
|
1050
1167
|
"""
|
|
1051
1168
|
Function Description:
|
|
@@ -1059,7 +1176,7 @@ def file_has_changed(source_path: str, destination_path: str) -> bool:
|
|
|
1059
1176
|
A boolean indicating whether the files are different
|
|
1060
1177
|
"""
|
|
1061
1178
|
|
|
1062
|
-
|
|
1179
|
+
|
|
1063
1180
|
return not filecmp.cmp(source_path, destination_path, shallow=False)
|
|
1064
1181
|
|
|
1065
1182
|
|
|
@@ -1548,29 +1665,134 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1548
1665
|
|
|
1549
1666
|
sys.stdout.flush()
|
|
1550
1667
|
|
|
1668
|
+
# Enable bracketed paste mode - terminal will wrap pastes with escape sequences
|
|
1669
|
+
sys.stdout.write('\033[?2004h')
|
|
1670
|
+
|
|
1551
1671
|
# Print prompt and reserve hint line
|
|
1552
1672
|
sys.stdout.write(prompt + '\n' + (token_hint or '') + '\033[A\r')
|
|
1553
1673
|
if prompt_visible_len > 0:
|
|
1554
1674
|
sys.stdout.write('\033[' + str(prompt_visible_len) + 'C')
|
|
1555
1675
|
sys.stdout.flush()
|
|
1556
1676
|
|
|
1677
|
+
# Store pasted content separately
|
|
1678
|
+
pasted_content = None
|
|
1679
|
+
in_paste = False
|
|
1680
|
+
paste_buffer = ""
|
|
1681
|
+
|
|
1682
|
+
# Track Ctrl+C for double-press exit
|
|
1683
|
+
import time
|
|
1684
|
+
last_ctrl_c_time = 0
|
|
1685
|
+
|
|
1557
1686
|
try:
|
|
1558
1687
|
tty.setcbreak(fd)
|
|
1688
|
+
|
|
1559
1689
|
while True:
|
|
1560
1690
|
c = sys.stdin.read(1)
|
|
1561
1691
|
|
|
1562
|
-
if c
|
|
1563
|
-
#
|
|
1692
|
+
if not c: # EOF/stdin closed
|
|
1693
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste
|
|
1564
1694
|
sys.stdout.write('\n\033[K')
|
|
1565
1695
|
sys.stdout.flush()
|
|
1566
|
-
|
|
1567
|
-
readline.add_history(buf)
|
|
1568
|
-
return buf
|
|
1696
|
+
raise EOFError
|
|
1569
1697
|
|
|
1570
|
-
|
|
1698
|
+
# Check for bracketed paste start: ESC [ 2 0 0 ~
|
|
1699
|
+
if c == '\x1b':
|
|
1571
1700
|
c2 = sys.stdin.read(1)
|
|
1572
1701
|
if c2 == '[':
|
|
1573
1702
|
c3 = sys.stdin.read(1)
|
|
1703
|
+
if c3 == '2':
|
|
1704
|
+
c4 = sys.stdin.read(1)
|
|
1705
|
+
if c4 == '0':
|
|
1706
|
+
c5 = sys.stdin.read(1)
|
|
1707
|
+
if c5 == '0':
|
|
1708
|
+
c6 = sys.stdin.read(1)
|
|
1709
|
+
if c6 == '~':
|
|
1710
|
+
# Start of bracketed paste
|
|
1711
|
+
in_paste = True
|
|
1712
|
+
paste_buffer = ""
|
|
1713
|
+
continue
|
|
1714
|
+
elif c5 == '1':
|
|
1715
|
+
c6 = sys.stdin.read(1)
|
|
1716
|
+
if c6 == '~':
|
|
1717
|
+
# End of bracketed paste ESC [ 2 0 1 ~
|
|
1718
|
+
in_paste = False
|
|
1719
|
+
if paste_buffer:
|
|
1720
|
+
# Check if this looks like binary/image data
|
|
1721
|
+
# Image signatures: PNG (\x89PNG), JPEG (\xff\xd8\xff), GIF (GIF8), BMP (BM)
|
|
1722
|
+
# Also check for high ratio of non-printable chars
|
|
1723
|
+
is_binary = False
|
|
1724
|
+
if len(paste_buffer) > 4:
|
|
1725
|
+
# Check for common image magic bytes
|
|
1726
|
+
if paste_buffer[:4] == '\x89PNG' or paste_buffer[:8] == '\x89PNG\r\n\x1a\n':
|
|
1727
|
+
is_binary = True
|
|
1728
|
+
elif paste_buffer[:2] == '\xff\xd8': # JPEG
|
|
1729
|
+
is_binary = True
|
|
1730
|
+
elif paste_buffer[:4] == 'GIF8': # GIF
|
|
1731
|
+
is_binary = True
|
|
1732
|
+
elif paste_buffer[:2] == 'BM': # BMP
|
|
1733
|
+
is_binary = True
|
|
1734
|
+
elif paste_buffer.startswith('data:image/'): # Base64 data URL
|
|
1735
|
+
is_binary = True
|
|
1736
|
+
else:
|
|
1737
|
+
# Check for high ratio of non-printable characters
|
|
1738
|
+
non_printable = sum(1 for c in paste_buffer[:100] if ord(c) < 32 and c not in '\n\r\t')
|
|
1739
|
+
if non_printable > 10: # More than 10% non-printable in first 100 chars
|
|
1740
|
+
is_binary = True
|
|
1741
|
+
|
|
1742
|
+
if is_binary:
|
|
1743
|
+
# Save image data to temp file
|
|
1744
|
+
import tempfile
|
|
1745
|
+
import os
|
|
1746
|
+
try:
|
|
1747
|
+
# Determine extension from magic bytes
|
|
1748
|
+
ext = '.bin'
|
|
1749
|
+
if '\x89PNG' in paste_buffer[:8]:
|
|
1750
|
+
ext = '.png'
|
|
1751
|
+
elif paste_buffer[:2] == '\xff\xd8':
|
|
1752
|
+
ext = '.jpg'
|
|
1753
|
+
elif paste_buffer[:4] == 'GIF8':
|
|
1754
|
+
ext = '.gif'
|
|
1755
|
+
elif paste_buffer.startswith('data:image/'):
|
|
1756
|
+
# Extract from data URL
|
|
1757
|
+
if 'png' in paste_buffer[:30]:
|
|
1758
|
+
ext = '.png'
|
|
1759
|
+
elif 'jpeg' in paste_buffer[:30] or 'jpg' in paste_buffer[:30]:
|
|
1760
|
+
ext = '.jpg'
|
|
1761
|
+
elif 'gif' in paste_buffer[:30]:
|
|
1762
|
+
ext = '.gif'
|
|
1763
|
+
|
|
1764
|
+
fd, temp_path = tempfile.mkstemp(suffix=ext, prefix='npcsh_paste_')
|
|
1765
|
+
with os.fdopen(fd, 'wb') as f:
|
|
1766
|
+
if paste_buffer.startswith('data:image/'):
|
|
1767
|
+
# Decode base64 data URL
|
|
1768
|
+
import base64
|
|
1769
|
+
_, data = paste_buffer.split(',', 1)
|
|
1770
|
+
f.write(base64.b64decode(data))
|
|
1771
|
+
else:
|
|
1772
|
+
f.write(paste_buffer.encode('latin-1'))
|
|
1773
|
+
pasted_content = temp_path # Store path to image
|
|
1774
|
+
placeholder = f"[pasted image: {temp_path}]"
|
|
1775
|
+
except:
|
|
1776
|
+
pasted_content = None
|
|
1777
|
+
placeholder = "[pasted image: failed to save]"
|
|
1778
|
+
else:
|
|
1779
|
+
pasted_content = paste_buffer.rstrip('\r\n')
|
|
1780
|
+
line_count = pasted_content.count('\n') + 1
|
|
1781
|
+
char_count = len(pasted_content)
|
|
1782
|
+
if line_count > 1:
|
|
1783
|
+
placeholder = f"[pasted: {line_count} lines, {char_count} chars]"
|
|
1784
|
+
else:
|
|
1785
|
+
# Single line paste - just insert it directly
|
|
1786
|
+
buf = buf[:pos] + pasted_content + buf[pos:]
|
|
1787
|
+
pos += len(pasted_content)
|
|
1788
|
+
pasted_content = None # Clear so we don't replace on submit
|
|
1789
|
+
draw()
|
|
1790
|
+
continue
|
|
1791
|
+
buf = buf[:pos] + placeholder + buf[pos:]
|
|
1792
|
+
pos += len(placeholder)
|
|
1793
|
+
draw()
|
|
1794
|
+
continue
|
|
1795
|
+
# Handle arrow keys and other escape sequences
|
|
1574
1796
|
if c3 == 'A': # Up
|
|
1575
1797
|
if history_idx > 0:
|
|
1576
1798
|
if history_idx == len(history):
|
|
@@ -1579,37 +1801,78 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1579
1801
|
buf = history[history_idx] or ''
|
|
1580
1802
|
pos = len(buf)
|
|
1581
1803
|
draw()
|
|
1804
|
+
continue
|
|
1582
1805
|
elif c3 == 'B': # Down
|
|
1583
1806
|
if history_idx < len(history):
|
|
1584
1807
|
history_idx += 1
|
|
1585
1808
|
buf = saved_line if history_idx == len(history) else (history[history_idx] or '')
|
|
1586
1809
|
pos = len(buf)
|
|
1587
1810
|
draw()
|
|
1811
|
+
continue
|
|
1588
1812
|
elif c3 == 'C': # Right
|
|
1589
1813
|
if pos < len(buf):
|
|
1590
1814
|
pos += 1
|
|
1591
1815
|
sys.stdout.write('\033[C')
|
|
1592
1816
|
sys.stdout.flush()
|
|
1817
|
+
continue
|
|
1593
1818
|
elif c3 == 'D': # Left
|
|
1594
1819
|
if pos > 0:
|
|
1595
1820
|
pos -= 1
|
|
1596
1821
|
sys.stdout.write('\033[D')
|
|
1597
1822
|
sys.stdout.flush()
|
|
1823
|
+
continue
|
|
1598
1824
|
elif c3 == '3': # Del
|
|
1599
1825
|
sys.stdin.read(1) # ~
|
|
1600
1826
|
if pos < len(buf):
|
|
1601
1827
|
buf = buf[:pos] + buf[pos+1:]
|
|
1602
1828
|
draw()
|
|
1829
|
+
continue
|
|
1603
1830
|
elif c3 == 'H': # Home
|
|
1604
1831
|
pos = 0
|
|
1605
1832
|
draw()
|
|
1833
|
+
continue
|
|
1606
1834
|
elif c3 == 'F': # End
|
|
1607
1835
|
pos = len(buf)
|
|
1608
1836
|
draw()
|
|
1837
|
+
continue
|
|
1609
1838
|
elif c2 == '\x1b': # Double ESC
|
|
1839
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste
|
|
1610
1840
|
sys.stdout.write('\n\033[K')
|
|
1611
1841
|
sys.stdout.flush()
|
|
1612
1842
|
return '\x1b'
|
|
1843
|
+
continue
|
|
1844
|
+
|
|
1845
|
+
# If we're in a paste, accumulate to paste buffer
|
|
1846
|
+
if in_paste:
|
|
1847
|
+
paste_buffer += c
|
|
1848
|
+
continue
|
|
1849
|
+
|
|
1850
|
+
if c in ('\n', '\r'):
|
|
1851
|
+
# Clear hint and newline
|
|
1852
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste
|
|
1853
|
+
sys.stdout.write('\n\033[K')
|
|
1854
|
+
sys.stdout.flush()
|
|
1855
|
+
# If we have pasted content, replace placeholder with actual content
|
|
1856
|
+
if pasted_content is not None:
|
|
1857
|
+
import re
|
|
1858
|
+
# Escape pipe characters in pasted content so they aren't parsed as pipeline operators
|
|
1859
|
+
escaped_content = pasted_content.replace('|', '\\|')
|
|
1860
|
+
# If pasted content is at the start of command, escape @ and / to prevent
|
|
1861
|
+
# them being interpreted as delegation or slash commands
|
|
1862
|
+
placeholder_pattern = r'\[pasted: \d+ lines?, \d+ chars?\]'
|
|
1863
|
+
if re.match(placeholder_pattern, buf.lstrip()):
|
|
1864
|
+
if escaped_content.startswith('@'):
|
|
1865
|
+
escaped_content = '\\@' + escaped_content[1:]
|
|
1866
|
+
elif escaped_content.startswith('/'):
|
|
1867
|
+
escaped_content = '\\/' + escaped_content[1:]
|
|
1868
|
+
# Use lambda to avoid backreference issues in replacement string
|
|
1869
|
+
result = re.sub(placeholder_pattern, lambda m: escaped_content, buf)
|
|
1870
|
+
if result.strip():
|
|
1871
|
+
readline.add_history(result)
|
|
1872
|
+
return result
|
|
1873
|
+
if buf.strip():
|
|
1874
|
+
readline.add_history(buf)
|
|
1875
|
+
return buf
|
|
1613
1876
|
|
|
1614
1877
|
elif c == '\x7f' or c == '\x08': # Backspace
|
|
1615
1878
|
if pos > 0:
|
|
@@ -1618,9 +1881,27 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1618
1881
|
draw()
|
|
1619
1882
|
|
|
1620
1883
|
elif c == '\x03': # Ctrl-C
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1884
|
+
current_time = time.time()
|
|
1885
|
+
if current_time - last_ctrl_c_time < 1.0: # Double Ctrl+C within 1 second
|
|
1886
|
+
# Exit
|
|
1887
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste
|
|
1888
|
+
sys.stdout.write('\n\033[K')
|
|
1889
|
+
sys.stdout.flush()
|
|
1890
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
1891
|
+
raise KeyboardInterrupt
|
|
1892
|
+
else:
|
|
1893
|
+
# First Ctrl+C - clear the line
|
|
1894
|
+
last_ctrl_c_time = current_time
|
|
1895
|
+
buf = ""
|
|
1896
|
+
pos = 0
|
|
1897
|
+
pasted_content = None
|
|
1898
|
+
sys.stdout.write('\r\033[K') # Clear current line
|
|
1899
|
+
sys.stdout.write('^C\n')
|
|
1900
|
+
# Redraw prompt
|
|
1901
|
+
sys.stdout.write(prompt + '\n' + current_hint() + '\033[A\r')
|
|
1902
|
+
if prompt_visible_len > 0:
|
|
1903
|
+
sys.stdout.write('\033[' + str(prompt_visible_len) + 'C')
|
|
1904
|
+
sys.stdout.flush()
|
|
1624
1905
|
|
|
1625
1906
|
elif c == '\x04': # Ctrl-D
|
|
1626
1907
|
if not buf:
|
|
@@ -1692,12 +1973,14 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1692
1973
|
except:
|
|
1693
1974
|
pass
|
|
1694
1975
|
|
|
1695
|
-
elif ord(c) >= 32: # Printable
|
|
1976
|
+
elif c and ord(c) >= 32: # Printable
|
|
1696
1977
|
buf = buf[:pos] + c + buf[pos:]
|
|
1697
1978
|
pos += 1
|
|
1698
1979
|
draw()
|
|
1699
1980
|
|
|
1700
1981
|
finally:
|
|
1982
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste mode
|
|
1983
|
+
sys.stdout.flush()
|
|
1701
1984
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
1702
1985
|
|
|
1703
1986
|
|
|
@@ -2123,6 +2406,96 @@ def model_supports_tool_calls(model: Optional[str], provider: Optional[str]) ->
|
|
|
2123
2406
|
return any(marker in model_lower for marker in toolish_markers)
|
|
2124
2407
|
|
|
2125
2408
|
|
|
2409
|
+
def wrap_tool_with_display(tool_name: str, tool_func: Callable, state: ShellState) -> Callable:
|
|
2410
|
+
"""Wrap a tool function to add visual feedback when it executes.
|
|
2411
|
+
|
|
2412
|
+
Respects state.log_level:
|
|
2413
|
+
- "silent": no output
|
|
2414
|
+
- "normal": show tool name and success/failure
|
|
2415
|
+
- "verbose": show tool name, args, success/failure, and result preview
|
|
2416
|
+
"""
|
|
2417
|
+
def wrapped(**kwargs):
|
|
2418
|
+
log_level = getattr(state, 'log_level', 'normal')
|
|
2419
|
+
|
|
2420
|
+
# Display tool call (skip in silent mode)
|
|
2421
|
+
if log_level != "silent":
|
|
2422
|
+
try:
|
|
2423
|
+
args_display = ""
|
|
2424
|
+
# Always show a preview of args for key tools
|
|
2425
|
+
if kwargs:
|
|
2426
|
+
# For sh/python/sql, show the code/command being run
|
|
2427
|
+
if tool_name in ('sh', 'python', 'sql', 'cmd') and 'code' in kwargs:
|
|
2428
|
+
code_preview = str(kwargs['code']).strip().split('\n')[0] # First line
|
|
2429
|
+
if len(code_preview) > 80:
|
|
2430
|
+
code_preview = code_preview[:80] + "…"
|
|
2431
|
+
args_display = code_preview
|
|
2432
|
+
elif tool_name == 'sh' and 'bash_command' in kwargs:
|
|
2433
|
+
cmd_preview = str(kwargs['bash_command']).strip().split('\n')[0]
|
|
2434
|
+
if len(cmd_preview) > 80:
|
|
2435
|
+
cmd_preview = cmd_preview[:80] + "…"
|
|
2436
|
+
args_display = cmd_preview
|
|
2437
|
+
elif tool_name in ('sh', 'cmd') and 'command' in kwargs:
|
|
2438
|
+
cmd_preview = str(kwargs['command']).strip().split('\n')[0]
|
|
2439
|
+
if len(cmd_preview) > 80:
|
|
2440
|
+
cmd_preview = cmd_preview[:80] + "…"
|
|
2441
|
+
args_display = cmd_preview
|
|
2442
|
+
elif tool_name == 'python' and 'python_code' in kwargs:
|
|
2443
|
+
code_preview = str(kwargs['python_code']).strip().split('\n')[0]
|
|
2444
|
+
if len(code_preview) > 80:
|
|
2445
|
+
code_preview = code_preview[:80] + "…"
|
|
2446
|
+
args_display = code_preview
|
|
2447
|
+
elif tool_name == 'agent' and 'npc_name' in kwargs:
|
|
2448
|
+
args_display = f"@{kwargs['npc_name']}"
|
|
2449
|
+
if 'request' in kwargs:
|
|
2450
|
+
req = str(kwargs['request'])[:50]
|
|
2451
|
+
args_display += f": {req}…" if len(str(kwargs['request'])) > 50 else f": {req}"
|
|
2452
|
+
elif tool_name == 'agent' and 'query' in kwargs:
|
|
2453
|
+
query_preview = str(kwargs['query']).strip()[:60]
|
|
2454
|
+
args_display = query_preview + ("…" if len(str(kwargs['query'])) > 60 else "")
|
|
2455
|
+
elif log_level == "verbose":
|
|
2456
|
+
arg_parts = []
|
|
2457
|
+
for _, v in kwargs.items():
|
|
2458
|
+
v_str = str(v)
|
|
2459
|
+
if len(v_str) > 40:
|
|
2460
|
+
v_str = v_str[:40] + "…"
|
|
2461
|
+
arg_parts.append(f"{v_str}")
|
|
2462
|
+
args_display = " ".join(arg_parts)
|
|
2463
|
+
if len(args_display) > 60:
|
|
2464
|
+
args_display = args_display[:60] + "…"
|
|
2465
|
+
|
|
2466
|
+
if args_display:
|
|
2467
|
+
print(colored(f" ⚡ {tool_name}", "cyan") + colored(f" {args_display}", "white", attrs=["dark"]), end="", flush=True)
|
|
2468
|
+
else:
|
|
2469
|
+
print(colored(f" ⚡ {tool_name}", "cyan"), end="", flush=True)
|
|
2470
|
+
except:
|
|
2471
|
+
pass
|
|
2472
|
+
|
|
2473
|
+
# Execute tool
|
|
2474
|
+
try:
|
|
2475
|
+
result = tool_func(**kwargs)
|
|
2476
|
+
if log_level != "silent":
|
|
2477
|
+
try:
|
|
2478
|
+
print(colored(" ✓", "green"), flush=True)
|
|
2479
|
+
# Show preview of result only in verbose mode
|
|
2480
|
+
if log_level == "verbose":
|
|
2481
|
+
result_preview = str(result)
|
|
2482
|
+
if len(result_preview) > 200:
|
|
2483
|
+
result_preview = result_preview[:200] + "..."
|
|
2484
|
+
if result_preview and result_preview not in ('None', '', '{}', '[]'):
|
|
2485
|
+
print(colored(f" → {result_preview}", "white", attrs=["dark"]), flush=True)
|
|
2486
|
+
except:
|
|
2487
|
+
pass
|
|
2488
|
+
return result
|
|
2489
|
+
except Exception as e:
|
|
2490
|
+
if log_level != "silent":
|
|
2491
|
+
try:
|
|
2492
|
+
print(colored(f" ✗ {str(e)[:100]}", "red"), flush=True)
|
|
2493
|
+
except:
|
|
2494
|
+
pass
|
|
2495
|
+
raise
|
|
2496
|
+
return wrapped
|
|
2497
|
+
|
|
2498
|
+
|
|
2126
2499
|
def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str, Callable]]:
|
|
2127
2500
|
"""
|
|
2128
2501
|
Assemble tool definitions + executable map from NPC tools, Jinxs, and MCP servers.
|
|
@@ -2230,7 +2603,11 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
|
|
|
2230
2603
|
name = tool_def.get("function", {}).get("name")
|
|
2231
2604
|
if name:
|
|
2232
2605
|
deduped[name] = tool_def
|
|
2233
|
-
|
|
2606
|
+
|
|
2607
|
+
# Wrap all tools with display feedback for npcsh
|
|
2608
|
+
wrapped_tool_map = {name: wrap_tool_with_display(name, func, state) for name, func in tool_map.items()}
|
|
2609
|
+
|
|
2610
|
+
return list(deduped.values()), wrapped_tool_map
|
|
2234
2611
|
|
|
2235
2612
|
|
|
2236
2613
|
def should_skip_kg_processing(user_input: str, assistant_output: str) -> bool:
|
|
@@ -2454,15 +2831,19 @@ def process_pipeline_command(
|
|
|
2454
2831
|
else cmd_to_process
|
|
2455
2832
|
)
|
|
2456
2833
|
path_cmd = 'The current working directory is: ' + state.current_path
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2834
|
+
if os.path.exists(state.current_path):
|
|
2835
|
+
all_files = os.listdir(state.current_path)
|
|
2836
|
+
# Limit to first 100 files to avoid token explosion
|
|
2837
|
+
limited_files = all_files[:100]
|
|
2838
|
+
file_list = "\n".join([
|
|
2839
|
+
os.path.join(state.current_path, f)
|
|
2840
|
+
for f in limited_files
|
|
2841
|
+
])
|
|
2842
|
+
if len(all_files) > 100:
|
|
2843
|
+
file_list += f"\n... and {len(all_files) - 100} more files"
|
|
2844
|
+
ls_files = 'Files in the current directory (full paths):\n' + file_list
|
|
2845
|
+
else:
|
|
2846
|
+
ls_files = 'No files found in the current directory.'
|
|
2466
2847
|
platform_info = (
|
|
2467
2848
|
f"Platform: {platform.system()} {platform.release()} "
|
|
2468
2849
|
f"({platform.machine()})"
|
|
@@ -2526,18 +2907,66 @@ def process_pipeline_command(
|
|
|
2526
2907
|
(exec_model and "gemini" in exec_model.lower())
|
|
2527
2908
|
llm_kwargs["tool_choice"] = 'auto'
|
|
2528
2909
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2910
|
+
# Agent loop: keep calling LLM until it stops making tool calls
|
|
2911
|
+
# The LLM decides when it's done - npcsh just facilitates
|
|
2912
|
+
iteration = 0
|
|
2913
|
+
max_iterations = 50 # Safety limit to prevent infinite loops
|
|
2914
|
+
total_usage = {"input_tokens": 0, "output_tokens": 0}
|
|
2915
|
+
|
|
2916
|
+
while iteration < max_iterations:
|
|
2917
|
+
iteration += 1
|
|
2918
|
+
|
|
2919
|
+
llm_result = get_llm_response(
|
|
2920
|
+
full_llm_cmd if iteration == 1 else None, # Only pass prompt on first call
|
|
2921
|
+
model=exec_model,
|
|
2922
|
+
provider=exec_provider,
|
|
2923
|
+
npc=state.npc,
|
|
2924
|
+
team=state.team,
|
|
2925
|
+
messages=state.messages,
|
|
2926
|
+
stream=False, # Don't stream intermediate calls
|
|
2927
|
+
attachments=state.attachments if iteration == 1 else None,
|
|
2928
|
+
context=info if iteration == 1 else None,
|
|
2929
|
+
**llm_kwargs,
|
|
2930
|
+
)
|
|
2931
|
+
|
|
2932
|
+
# Accumulate usage
|
|
2933
|
+
if isinstance(llm_result, dict) and llm_result.get('usage'):
|
|
2934
|
+
total_usage["input_tokens"] += llm_result['usage'].get('input_tokens', 0)
|
|
2935
|
+
total_usage["output_tokens"] += llm_result['usage'].get('output_tokens', 0)
|
|
2936
|
+
|
|
2937
|
+
# Update state messages
|
|
2938
|
+
old_msg_count = len(state.messages) if state.messages else 0
|
|
2939
|
+
if isinstance(llm_result, dict):
|
|
2940
|
+
state.messages = llm_result.get("messages", state.messages)
|
|
2941
|
+
|
|
2942
|
+
# Display tool outputs from this iteration
|
|
2943
|
+
for msg in state.messages[old_msg_count:]:
|
|
2944
|
+
if msg.get("role") == "tool":
|
|
2945
|
+
tool_name = msg.get("name", "tool")
|
|
2946
|
+
tool_content = msg.get("content", "")
|
|
2947
|
+
if tool_content and tool_content.strip():
|
|
2948
|
+
print(colored(f"\n⚡ {tool_name}:", "cyan"))
|
|
2949
|
+
lines = tool_content.split('\n')
|
|
2950
|
+
if len(lines) > 50:
|
|
2951
|
+
print('\n'.join(lines[:25]))
|
|
2952
|
+
print(colored(f"\n... ({len(lines) - 50} lines hidden) ...\n", "white", attrs=["dark"]))
|
|
2953
|
+
print('\n'.join(lines[-25:]))
|
|
2954
|
+
else:
|
|
2955
|
+
print(tool_content)
|
|
2956
|
+
|
|
2957
|
+
# Check if LLM made tool calls - if not, it's done
|
|
2958
|
+
tool_calls_made = isinstance(llm_result, dict) and llm_result.get("tool_calls")
|
|
2959
|
+
if not tool_calls_made:
|
|
2960
|
+
# LLM is done - no more tool calls
|
|
2961
|
+
break
|
|
2962
|
+
|
|
2963
|
+
# Clear the prompt for continuation calls - context is in messages
|
|
2964
|
+
full_llm_cmd = None
|
|
2965
|
+
|
|
2966
|
+
# Store accumulated usage
|
|
2967
|
+
if isinstance(llm_result, dict):
|
|
2968
|
+
llm_result['usage'] = total_usage
|
|
2969
|
+
|
|
2541
2970
|
else:
|
|
2542
2971
|
llm_result = check_llm_command(
|
|
2543
2972
|
full_llm_cmd,
|
|
@@ -2781,6 +3210,11 @@ def execute_command(
|
|
|
2781
3210
|
if not command.strip():
|
|
2782
3211
|
return state, ""
|
|
2783
3212
|
|
|
3213
|
+
# Unescape @ and / at start of command that were escaped to prevent misinterpretation
|
|
3214
|
+
# (e.g., from pasted content that starts with @ or /)
|
|
3215
|
+
if command.startswith('\\@') or command.startswith('\\/'):
|
|
3216
|
+
command = command[1:] # Remove the escape backslash
|
|
3217
|
+
|
|
2784
3218
|
# Check for mode switch commands
|
|
2785
3219
|
mode_change, state = check_mode_switch(command, state)
|
|
2786
3220
|
if mode_change:
|
|
@@ -2802,6 +3236,8 @@ def execute_command(
|
|
|
2802
3236
|
|
|
2803
3237
|
original_command_for_embedding = command
|
|
2804
3238
|
commands = split_by_pipes(command)
|
|
3239
|
+
# Unescape pipe characters that were escaped to prevent splitting (e.g., from pasted content)
|
|
3240
|
+
commands = [cmd.replace('\\|', '|') for cmd in commands]
|
|
2805
3241
|
|
|
2806
3242
|
stdin_for_next = None
|
|
2807
3243
|
final_output = None
|
|
@@ -2993,32 +3429,44 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
2993
3429
|
default_forenpc_name = "forenpc"
|
|
2994
3430
|
else:
|
|
2995
3431
|
if not os.path.exists('.npcsh_global'):
|
|
2996
|
-
|
|
3432
|
+
try:
|
|
3433
|
+
resp = input(f"No npc_team found in {os.getcwd()}. Create a new team here? [Y/n]: ").strip().lower()
|
|
3434
|
+
except (KeyboardInterrupt, EOFError):
|
|
3435
|
+
print("\nAborted.")
|
|
3436
|
+
sys.exit(0)
|
|
2997
3437
|
if resp in ("", "y", "yes"):
|
|
2998
3438
|
team_dir = project_team_path
|
|
2999
3439
|
os.makedirs(team_dir, exist_ok=True)
|
|
3000
3440
|
default_forenpc_name = "forenpc"
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3441
|
+
try:
|
|
3442
|
+
forenpc_directive = input(
|
|
3443
|
+
f"Enter a primary directive for {default_forenpc_name} (default: 'You are the forenpc of the team...'): "
|
|
3444
|
+
).strip() or "You are the forenpc of the team, coordinating activities between NPCs on the team, verifying that results from NPCs are high quality and can help to adequately answer user requests."
|
|
3445
|
+
forenpc_model = input("Enter a model for your forenpc (default: llama3.2): ").strip() or "llama3.2"
|
|
3446
|
+
forenpc_provider = input("Enter a provider for your forenpc (default: ollama): ").strip() or "ollama"
|
|
3447
|
+
except (KeyboardInterrupt, EOFError):
|
|
3448
|
+
print("\nAborted.")
|
|
3449
|
+
sys.exit(0)
|
|
3450
|
+
|
|
3007
3451
|
with open(os.path.join(team_dir, f"{default_forenpc_name}.npc"), "w") as f:
|
|
3008
3452
|
yaml.dump({
|
|
3009
3453
|
"name": default_forenpc_name, "primary_directive": forenpc_directive,
|
|
3010
3454
|
"model": forenpc_model, "provider": forenpc_provider
|
|
3011
3455
|
}, f)
|
|
3012
|
-
|
|
3456
|
+
|
|
3013
3457
|
ctx_path = os.path.join(team_dir, "team.ctx")
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3458
|
+
try:
|
|
3459
|
+
folder_context = input("Enter a short description for this project/team (optional): ").strip()
|
|
3460
|
+
team_ctx_data = {
|
|
3461
|
+
"forenpc": default_forenpc_name,
|
|
3462
|
+
"model": forenpc_model,
|
|
3463
|
+
"provider": forenpc_provider,
|
|
3464
|
+
"context": folder_context if folder_context else None
|
|
3465
|
+
}
|
|
3466
|
+
use_jinxs = input("Use global jinxs folder (g) or copy to this project (c)? [g/c, default: g]: ").strip().lower()
|
|
3467
|
+
except (KeyboardInterrupt, EOFError):
|
|
3468
|
+
print("\nAborted.")
|
|
3469
|
+
sys.exit(0)
|
|
3022
3470
|
if use_jinxs == "c":
|
|
3023
3471
|
global_jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
|
|
3024
3472
|
if os.path.exists(global_jinxs_dir):
|
|
@@ -3035,7 +3483,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
3035
3483
|
with open(".npcsh_global", "w") as f:
|
|
3036
3484
|
pass
|
|
3037
3485
|
team_dir = global_team_path
|
|
3038
|
-
default_forenpc_name = "sibiji"
|
|
3486
|
+
default_forenpc_name = "sibiji"
|
|
3039
3487
|
else:
|
|
3040
3488
|
team_dir = global_team_path
|
|
3041
3489
|
default_forenpc_name = "sibiji"
|
|
@@ -3195,7 +3643,8 @@ def process_result(
|
|
|
3195
3643
|
|
|
3196
3644
|
# FIX: Handle dict output properly
|
|
3197
3645
|
if isinstance(output, dict):
|
|
3198
|
-
|
|
3646
|
+
# Use None-safe check to not skip empty strings
|
|
3647
|
+
output_content = output.get('output') if 'output' in output else output.get('response')
|
|
3199
3648
|
model_for_stream = output.get('model', active_npc.model)
|
|
3200
3649
|
provider_for_stream = output.get('provider', active_npc.provider)
|
|
3201
3650
|
|
|
@@ -3212,18 +3661,22 @@ def process_result(
|
|
|
3212
3661
|
usage.get('output_tokens', 0)
|
|
3213
3662
|
)
|
|
3214
3663
|
|
|
3215
|
-
# If output_content is still a dict
|
|
3664
|
+
# If output_content is still a dict, convert to string
|
|
3216
3665
|
if isinstance(output_content, dict):
|
|
3217
3666
|
output_content = str(output_content)
|
|
3218
|
-
elif output_content is None:
|
|
3219
|
-
|
|
3667
|
+
elif output_content is None or output_content == '':
|
|
3668
|
+
# No output from the agent - this is fine, don't show annoying message
|
|
3669
|
+
output_content = None
|
|
3220
3670
|
else:
|
|
3221
3671
|
output_content = output
|
|
3222
3672
|
model_for_stream = active_npc.model
|
|
3223
3673
|
provider_for_stream = active_npc.provider
|
|
3224
3674
|
|
|
3225
3675
|
print('\n')
|
|
3226
|
-
if
|
|
3676
|
+
if output_content is None:
|
|
3677
|
+
# No output to display - tool results already shown during execution
|
|
3678
|
+
pass
|
|
3679
|
+
elif user_input == '/help':
|
|
3227
3680
|
if isinstance(output_content, str):
|
|
3228
3681
|
render_markdown(output_content)
|
|
3229
3682
|
else:
|
|
@@ -3235,12 +3688,12 @@ def process_result(
|
|
|
3235
3688
|
render_markdown(final_output_str)
|
|
3236
3689
|
else:
|
|
3237
3690
|
final_output_str = print_and_process_stream_with_markdown(
|
|
3238
|
-
output_content,
|
|
3239
|
-
model_for_stream,
|
|
3240
|
-
provider_for_stream,
|
|
3691
|
+
output_content,
|
|
3692
|
+
model_for_stream,
|
|
3693
|
+
provider_for_stream,
|
|
3241
3694
|
show=True
|
|
3242
3695
|
)
|
|
3243
|
-
|
|
3696
|
+
else:
|
|
3244
3697
|
final_output_str = str(output_content)
|
|
3245
3698
|
render_markdown(final_output_str)
|
|
3246
3699
|
|