npcsh 1.1.13__py3-none-any.whl → 1.1.15__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 +491 -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.13.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.13.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/edit_file.jinx +3 -3
- {npcsh-1.1.13.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.13.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.15.data/data/npcsh/npc_team/alicanto.npc +23 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/arxiv.jinx +76 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/browser_action.jinx +220 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/browser_screenshot.jinx +40 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/build.jinx +8 -8
- npcsh-1.1.15.data/data/npcsh/npc_team/click.jinx +23 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/close_browser.jinx +14 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/convene.jinx +232 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/corca.npc +31 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/delegate.jinx +184 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.15.data/data/npcsh/npc_team}/edit_file.jinx +3 -3
- npcsh-1.1.15.data/data/npcsh/npc_team/frederic.npc +27 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/guac.npc +22 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/jinxs.jinx +176 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/kadiefa.npc +21 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/key_press.jinx +26 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/launch_app.jinx +37 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.15.data/data/npcsh/npc_team}/load_file.jinx +1 -1
- npcsh-1.1.15.data/data/npcsh/npc_team/nql.jinx +141 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/open_browser.jinx +43 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/paper_search.jinx +101 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/paste.jinx +134 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/plonk.npc +27 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/plonkjr.npc +23 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/screenshot.jinx +23 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.15.data/data/npcsh/npc_team}/search.jinx +2 -1
- npcsh-1.1.15.data/data/npcsh/npc_team/semantic_scholar.jinx +69 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sh.jinx +2 -8
- npcsh-1.1.15.data/data/npcsh/npc_team/shh.jinx +17 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/sibiji.npc +24 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sql.jinx +1 -1
- npcsh-1.1.15.data/data/npcsh/npc_team/switch.jinx +62 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/switches.jinx +61 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/sync.jinx +230 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/teamviz.jinx +205 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/type_text.jinx +27 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/verbose.jinx +17 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.15.data/data/npcsh/npc_team}/vixynt.jinx +8 -30
- npcsh-1.1.15.data/data/npcsh/npc_team/wait.jinx +21 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/wander.jinx +152 -0
- {npcsh-1.1.13.dist-info → npcsh-1.1.15.dist-info}/METADATA +399 -58
- npcsh-1.1.15.dist-info/RECORD +170 -0
- npcsh-1.1.15.dist-info/entry_points.txt +19 -0
- npcsh-1.1.15.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.13.data/data/npcsh/npc_team/agent.jinx +0 -17
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.jinx +0 -194
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.npc +0 -2
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.jinx +0 -249
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.npc +0 -12
- npcsh-1.1.13.data/data/npcsh/npc_team/foreman.npc +0 -7
- npcsh-1.1.13.data/data/npcsh/npc_team/frederic.npc +0 -6
- npcsh-1.1.13.data/data/npcsh/npc_team/guac.jinx +0 -317
- npcsh-1.1.13.data/data/npcsh/npc_team/jinxs.jinx +0 -32
- npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.npc +0 -3
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.jinx +0 -214
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.npc +0 -2
- npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.npc +0 -2
- npcsh-1.1.13.data/data/npcsh/npc_team/pti.jinx +0 -170
- npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.npc +0 -3
- npcsh-1.1.13.data/data/npcsh/npc_team/wander.jinx +0 -186
- npcsh-1.1.13.dist-info/RECORD +0 -135
- npcsh-1.1.13.dist-info/entry_points.txt +0 -9
- npcsh-1.1.13.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.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/yap.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.13.dist-info → npcsh-1.1.15.dist-info}/WHEEL +0 -0
- {npcsh-1.1.13.dist-info → npcsh-1.1.15.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",
|
|
@@ -369,31 +399,76 @@ def initialize_base_npcs_if_needed(db_path: str) -> None:
|
|
|
369
399
|
shutil.copy2(source_path, destination_path)
|
|
370
400
|
print(f"Copied ctx {filename} to {destination_path}")
|
|
371
401
|
|
|
372
|
-
# Copy jinxs directory RECURSIVELY
|
|
402
|
+
# Copy jinxs directory RECURSIVELY with manifest tracking
|
|
403
|
+
# This ensures we only sync package jinxs and can clean up old ones
|
|
373
404
|
package_jinxs_dir = os.path.join(package_npc_team_dir, "jinxs")
|
|
405
|
+
manifest_path = os.path.join(user_jinxs_dir, ".package_manifest.json")
|
|
406
|
+
|
|
407
|
+
# Load existing manifest of package-synced jinxs
|
|
408
|
+
old_package_jinxs = set()
|
|
409
|
+
if os.path.exists(manifest_path):
|
|
410
|
+
try:
|
|
411
|
+
import json
|
|
412
|
+
with open(manifest_path, 'r') as f:
|
|
413
|
+
old_package_jinxs = set(json.load(f).get('jinxs', []))
|
|
414
|
+
except:
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
# Track current package jinxs
|
|
418
|
+
current_package_jinxs = set()
|
|
419
|
+
|
|
374
420
|
if os.path.exists(package_jinxs_dir):
|
|
375
421
|
for root, dirs, files in os.walk(package_jinxs_dir):
|
|
376
422
|
# Calculate relative path from package_jinxs_dir
|
|
377
423
|
rel_path = os.path.relpath(root, package_jinxs_dir)
|
|
378
|
-
|
|
424
|
+
|
|
379
425
|
# Create corresponding directory in user jinxs
|
|
380
426
|
if rel_path == '.':
|
|
381
427
|
dest_dir = user_jinxs_dir
|
|
382
428
|
else:
|
|
383
429
|
dest_dir = os.path.join(user_jinxs_dir, rel_path)
|
|
384
430
|
os.makedirs(dest_dir, exist_ok=True)
|
|
385
|
-
|
|
431
|
+
|
|
386
432
|
# Copy all .jinx files in this directory
|
|
387
433
|
for filename in files:
|
|
388
434
|
if filename.endswith(".jinx"):
|
|
389
435
|
source_jinx_path = os.path.join(root, filename)
|
|
390
436
|
destination_jinx_path = os.path.join(dest_dir, filename)
|
|
391
|
-
|
|
437
|
+
jinx_rel_path = os.path.join(rel_path, filename) if rel_path != '.' else filename
|
|
438
|
+
current_package_jinxs.add(jinx_rel_path)
|
|
439
|
+
|
|
392
440
|
if not os.path.exists(destination_jinx_path) or file_has_changed(
|
|
393
441
|
source_jinx_path, destination_jinx_path
|
|
394
442
|
):
|
|
395
443
|
shutil.copy2(source_jinx_path, destination_jinx_path)
|
|
396
|
-
print(f"Copied jinx {
|
|
444
|
+
print(f"Copied jinx {jinx_rel_path} to {destination_jinx_path}")
|
|
445
|
+
|
|
446
|
+
# Clean up old package jinxs that are no longer in the package
|
|
447
|
+
# (but preserve user-created jinxs that were never in the manifest)
|
|
448
|
+
stale_jinxs = old_package_jinxs - current_package_jinxs
|
|
449
|
+
for stale_jinx in stale_jinxs:
|
|
450
|
+
stale_path = os.path.join(user_jinxs_dir, stale_jinx)
|
|
451
|
+
if os.path.exists(stale_path):
|
|
452
|
+
try:
|
|
453
|
+
os.remove(stale_path)
|
|
454
|
+
print(f"Removed stale package jinx: {stale_jinx}")
|
|
455
|
+
# Remove empty parent directories
|
|
456
|
+
parent_dir = os.path.dirname(stale_path)
|
|
457
|
+
while parent_dir != user_jinxs_dir:
|
|
458
|
+
if os.path.isdir(parent_dir) and not os.listdir(parent_dir):
|
|
459
|
+
os.rmdir(parent_dir)
|
|
460
|
+
print(f"Removed empty directory: {parent_dir}")
|
|
461
|
+
parent_dir = os.path.dirname(parent_dir)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
print(f"Could not remove stale jinx {stale_jinx}: {e}")
|
|
464
|
+
|
|
465
|
+
# Save updated manifest
|
|
466
|
+
try:
|
|
467
|
+
import json
|
|
468
|
+
with open(manifest_path, 'w') as f:
|
|
469
|
+
json.dump({'jinxs': list(current_package_jinxs), 'updated': str(__import__('datetime').datetime.now())}, f, indent=2)
|
|
470
|
+
except Exception as e:
|
|
471
|
+
print(f"Could not save jinx manifest: {e}")
|
|
397
472
|
|
|
398
473
|
# Copy templates directory
|
|
399
474
|
templates = os.path.join(package_npc_team_dir, "templates")
|
|
@@ -1548,29 +1623,134 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1548
1623
|
|
|
1549
1624
|
sys.stdout.flush()
|
|
1550
1625
|
|
|
1626
|
+
# Enable bracketed paste mode - terminal will wrap pastes with escape sequences
|
|
1627
|
+
sys.stdout.write('\033[?2004h')
|
|
1628
|
+
|
|
1551
1629
|
# Print prompt and reserve hint line
|
|
1552
1630
|
sys.stdout.write(prompt + '\n' + (token_hint or '') + '\033[A\r')
|
|
1553
1631
|
if prompt_visible_len > 0:
|
|
1554
1632
|
sys.stdout.write('\033[' + str(prompt_visible_len) + 'C')
|
|
1555
1633
|
sys.stdout.flush()
|
|
1556
1634
|
|
|
1635
|
+
# Store pasted content separately
|
|
1636
|
+
pasted_content = None
|
|
1637
|
+
in_paste = False
|
|
1638
|
+
paste_buffer = ""
|
|
1639
|
+
|
|
1640
|
+
# Track Ctrl+C for double-press exit
|
|
1641
|
+
import time
|
|
1642
|
+
last_ctrl_c_time = 0
|
|
1643
|
+
|
|
1557
1644
|
try:
|
|
1558
1645
|
tty.setcbreak(fd)
|
|
1646
|
+
|
|
1559
1647
|
while True:
|
|
1560
1648
|
c = sys.stdin.read(1)
|
|
1561
1649
|
|
|
1562
|
-
if c
|
|
1563
|
-
#
|
|
1650
|
+
if not c: # EOF/stdin closed
|
|
1651
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste
|
|
1564
1652
|
sys.stdout.write('\n\033[K')
|
|
1565
1653
|
sys.stdout.flush()
|
|
1566
|
-
|
|
1567
|
-
readline.add_history(buf)
|
|
1568
|
-
return buf
|
|
1654
|
+
raise EOFError
|
|
1569
1655
|
|
|
1570
|
-
|
|
1656
|
+
# Check for bracketed paste start: ESC [ 2 0 0 ~
|
|
1657
|
+
if c == '\x1b':
|
|
1571
1658
|
c2 = sys.stdin.read(1)
|
|
1572
1659
|
if c2 == '[':
|
|
1573
1660
|
c3 = sys.stdin.read(1)
|
|
1661
|
+
if c3 == '2':
|
|
1662
|
+
c4 = sys.stdin.read(1)
|
|
1663
|
+
if c4 == '0':
|
|
1664
|
+
c5 = sys.stdin.read(1)
|
|
1665
|
+
if c5 == '0':
|
|
1666
|
+
c6 = sys.stdin.read(1)
|
|
1667
|
+
if c6 == '~':
|
|
1668
|
+
# Start of bracketed paste
|
|
1669
|
+
in_paste = True
|
|
1670
|
+
paste_buffer = ""
|
|
1671
|
+
continue
|
|
1672
|
+
elif c5 == '1':
|
|
1673
|
+
c6 = sys.stdin.read(1)
|
|
1674
|
+
if c6 == '~':
|
|
1675
|
+
# End of bracketed paste ESC [ 2 0 1 ~
|
|
1676
|
+
in_paste = False
|
|
1677
|
+
if paste_buffer:
|
|
1678
|
+
# Check if this looks like binary/image data
|
|
1679
|
+
# Image signatures: PNG (\x89PNG), JPEG (\xff\xd8\xff), GIF (GIF8), BMP (BM)
|
|
1680
|
+
# Also check for high ratio of non-printable chars
|
|
1681
|
+
is_binary = False
|
|
1682
|
+
if len(paste_buffer) > 4:
|
|
1683
|
+
# Check for common image magic bytes
|
|
1684
|
+
if paste_buffer[:4] == '\x89PNG' or paste_buffer[:8] == '\x89PNG\r\n\x1a\n':
|
|
1685
|
+
is_binary = True
|
|
1686
|
+
elif paste_buffer[:2] == '\xff\xd8': # JPEG
|
|
1687
|
+
is_binary = True
|
|
1688
|
+
elif paste_buffer[:4] == 'GIF8': # GIF
|
|
1689
|
+
is_binary = True
|
|
1690
|
+
elif paste_buffer[:2] == 'BM': # BMP
|
|
1691
|
+
is_binary = True
|
|
1692
|
+
elif paste_buffer.startswith('data:image/'): # Base64 data URL
|
|
1693
|
+
is_binary = True
|
|
1694
|
+
else:
|
|
1695
|
+
# Check for high ratio of non-printable characters
|
|
1696
|
+
non_printable = sum(1 for c in paste_buffer[:100] if ord(c) < 32 and c not in '\n\r\t')
|
|
1697
|
+
if non_printable > 10: # More than 10% non-printable in first 100 chars
|
|
1698
|
+
is_binary = True
|
|
1699
|
+
|
|
1700
|
+
if is_binary:
|
|
1701
|
+
# Save image data to temp file
|
|
1702
|
+
import tempfile
|
|
1703
|
+
import os
|
|
1704
|
+
try:
|
|
1705
|
+
# Determine extension from magic bytes
|
|
1706
|
+
ext = '.bin'
|
|
1707
|
+
if '\x89PNG' in paste_buffer[:8]:
|
|
1708
|
+
ext = '.png'
|
|
1709
|
+
elif paste_buffer[:2] == '\xff\xd8':
|
|
1710
|
+
ext = '.jpg'
|
|
1711
|
+
elif paste_buffer[:4] == 'GIF8':
|
|
1712
|
+
ext = '.gif'
|
|
1713
|
+
elif paste_buffer.startswith('data:image/'):
|
|
1714
|
+
# Extract from data URL
|
|
1715
|
+
if 'png' in paste_buffer[:30]:
|
|
1716
|
+
ext = '.png'
|
|
1717
|
+
elif 'jpeg' in paste_buffer[:30] or 'jpg' in paste_buffer[:30]:
|
|
1718
|
+
ext = '.jpg'
|
|
1719
|
+
elif 'gif' in paste_buffer[:30]:
|
|
1720
|
+
ext = '.gif'
|
|
1721
|
+
|
|
1722
|
+
fd, temp_path = tempfile.mkstemp(suffix=ext, prefix='npcsh_paste_')
|
|
1723
|
+
with os.fdopen(fd, 'wb') as f:
|
|
1724
|
+
if paste_buffer.startswith('data:image/'):
|
|
1725
|
+
# Decode base64 data URL
|
|
1726
|
+
import base64
|
|
1727
|
+
_, data = paste_buffer.split(',', 1)
|
|
1728
|
+
f.write(base64.b64decode(data))
|
|
1729
|
+
else:
|
|
1730
|
+
f.write(paste_buffer.encode('latin-1'))
|
|
1731
|
+
pasted_content = temp_path # Store path to image
|
|
1732
|
+
placeholder = f"[pasted image: {temp_path}]"
|
|
1733
|
+
except:
|
|
1734
|
+
pasted_content = None
|
|
1735
|
+
placeholder = "[pasted image: failed to save]"
|
|
1736
|
+
else:
|
|
1737
|
+
pasted_content = paste_buffer.rstrip('\r\n')
|
|
1738
|
+
line_count = pasted_content.count('\n') + 1
|
|
1739
|
+
char_count = len(pasted_content)
|
|
1740
|
+
if line_count > 1:
|
|
1741
|
+
placeholder = f"[pasted: {line_count} lines, {char_count} chars]"
|
|
1742
|
+
else:
|
|
1743
|
+
# Single line paste - just insert it directly
|
|
1744
|
+
buf = buf[:pos] + pasted_content + buf[pos:]
|
|
1745
|
+
pos += len(pasted_content)
|
|
1746
|
+
pasted_content = None # Clear so we don't replace on submit
|
|
1747
|
+
draw()
|
|
1748
|
+
continue
|
|
1749
|
+
buf = buf[:pos] + placeholder + buf[pos:]
|
|
1750
|
+
pos += len(placeholder)
|
|
1751
|
+
draw()
|
|
1752
|
+
continue
|
|
1753
|
+
# Handle arrow keys and other escape sequences
|
|
1574
1754
|
if c3 == 'A': # Up
|
|
1575
1755
|
if history_idx > 0:
|
|
1576
1756
|
if history_idx == len(history):
|
|
@@ -1579,37 +1759,78 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1579
1759
|
buf = history[history_idx] or ''
|
|
1580
1760
|
pos = len(buf)
|
|
1581
1761
|
draw()
|
|
1762
|
+
continue
|
|
1582
1763
|
elif c3 == 'B': # Down
|
|
1583
1764
|
if history_idx < len(history):
|
|
1584
1765
|
history_idx += 1
|
|
1585
1766
|
buf = saved_line if history_idx == len(history) else (history[history_idx] or '')
|
|
1586
1767
|
pos = len(buf)
|
|
1587
1768
|
draw()
|
|
1769
|
+
continue
|
|
1588
1770
|
elif c3 == 'C': # Right
|
|
1589
1771
|
if pos < len(buf):
|
|
1590
1772
|
pos += 1
|
|
1591
1773
|
sys.stdout.write('\033[C')
|
|
1592
1774
|
sys.stdout.flush()
|
|
1775
|
+
continue
|
|
1593
1776
|
elif c3 == 'D': # Left
|
|
1594
1777
|
if pos > 0:
|
|
1595
1778
|
pos -= 1
|
|
1596
1779
|
sys.stdout.write('\033[D')
|
|
1597
1780
|
sys.stdout.flush()
|
|
1781
|
+
continue
|
|
1598
1782
|
elif c3 == '3': # Del
|
|
1599
1783
|
sys.stdin.read(1) # ~
|
|
1600
1784
|
if pos < len(buf):
|
|
1601
1785
|
buf = buf[:pos] + buf[pos+1:]
|
|
1602
1786
|
draw()
|
|
1787
|
+
continue
|
|
1603
1788
|
elif c3 == 'H': # Home
|
|
1604
1789
|
pos = 0
|
|
1605
1790
|
draw()
|
|
1791
|
+
continue
|
|
1606
1792
|
elif c3 == 'F': # End
|
|
1607
1793
|
pos = len(buf)
|
|
1608
1794
|
draw()
|
|
1795
|
+
continue
|
|
1609
1796
|
elif c2 == '\x1b': # Double ESC
|
|
1797
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste
|
|
1610
1798
|
sys.stdout.write('\n\033[K')
|
|
1611
1799
|
sys.stdout.flush()
|
|
1612
1800
|
return '\x1b'
|
|
1801
|
+
continue
|
|
1802
|
+
|
|
1803
|
+
# If we're in a paste, accumulate to paste buffer
|
|
1804
|
+
if in_paste:
|
|
1805
|
+
paste_buffer += c
|
|
1806
|
+
continue
|
|
1807
|
+
|
|
1808
|
+
if c in ('\n', '\r'):
|
|
1809
|
+
# Clear hint and newline
|
|
1810
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste
|
|
1811
|
+
sys.stdout.write('\n\033[K')
|
|
1812
|
+
sys.stdout.flush()
|
|
1813
|
+
# If we have pasted content, replace placeholder with actual content
|
|
1814
|
+
if pasted_content is not None:
|
|
1815
|
+
import re
|
|
1816
|
+
# Escape pipe characters in pasted content so they aren't parsed as pipeline operators
|
|
1817
|
+
escaped_content = pasted_content.replace('|', '\\|')
|
|
1818
|
+
# If pasted content is at the start of command, escape @ and / to prevent
|
|
1819
|
+
# them being interpreted as delegation or slash commands
|
|
1820
|
+
placeholder_pattern = r'\[pasted: \d+ lines?, \d+ chars?\]'
|
|
1821
|
+
if re.match(placeholder_pattern, buf.lstrip()):
|
|
1822
|
+
if escaped_content.startswith('@'):
|
|
1823
|
+
escaped_content = '\\@' + escaped_content[1:]
|
|
1824
|
+
elif escaped_content.startswith('/'):
|
|
1825
|
+
escaped_content = '\\/' + escaped_content[1:]
|
|
1826
|
+
# Use lambda to avoid backreference issues in replacement string
|
|
1827
|
+
result = re.sub(placeholder_pattern, lambda m: escaped_content, buf)
|
|
1828
|
+
if result.strip():
|
|
1829
|
+
readline.add_history(result)
|
|
1830
|
+
return result
|
|
1831
|
+
if buf.strip():
|
|
1832
|
+
readline.add_history(buf)
|
|
1833
|
+
return buf
|
|
1613
1834
|
|
|
1614
1835
|
elif c == '\x7f' or c == '\x08': # Backspace
|
|
1615
1836
|
if pos > 0:
|
|
@@ -1618,9 +1839,27 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1618
1839
|
draw()
|
|
1619
1840
|
|
|
1620
1841
|
elif c == '\x03': # Ctrl-C
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1842
|
+
current_time = time.time()
|
|
1843
|
+
if current_time - last_ctrl_c_time < 1.0: # Double Ctrl+C within 1 second
|
|
1844
|
+
# Exit
|
|
1845
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste
|
|
1846
|
+
sys.stdout.write('\n\033[K')
|
|
1847
|
+
sys.stdout.flush()
|
|
1848
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
1849
|
+
raise KeyboardInterrupt
|
|
1850
|
+
else:
|
|
1851
|
+
# First Ctrl+C - clear the line
|
|
1852
|
+
last_ctrl_c_time = current_time
|
|
1853
|
+
buf = ""
|
|
1854
|
+
pos = 0
|
|
1855
|
+
pasted_content = None
|
|
1856
|
+
sys.stdout.write('\r\033[K') # Clear current line
|
|
1857
|
+
sys.stdout.write('^C\n')
|
|
1858
|
+
# Redraw prompt
|
|
1859
|
+
sys.stdout.write(prompt + '\n' + current_hint() + '\033[A\r')
|
|
1860
|
+
if prompt_visible_len > 0:
|
|
1861
|
+
sys.stdout.write('\033[' + str(prompt_visible_len) + 'C')
|
|
1862
|
+
sys.stdout.flush()
|
|
1624
1863
|
|
|
1625
1864
|
elif c == '\x04': # Ctrl-D
|
|
1626
1865
|
if not buf:
|
|
@@ -1692,12 +1931,14 @@ def _input_with_hint_below(prompt: str, state=None, router=None, token_hint: str
|
|
|
1692
1931
|
except:
|
|
1693
1932
|
pass
|
|
1694
1933
|
|
|
1695
|
-
elif ord(c) >= 32: # Printable
|
|
1934
|
+
elif c and ord(c) >= 32: # Printable
|
|
1696
1935
|
buf = buf[:pos] + c + buf[pos:]
|
|
1697
1936
|
pos += 1
|
|
1698
1937
|
draw()
|
|
1699
1938
|
|
|
1700
1939
|
finally:
|
|
1940
|
+
sys.stdout.write('\033[?2004l') # Disable bracketed paste mode
|
|
1941
|
+
sys.stdout.flush()
|
|
1701
1942
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
1702
1943
|
|
|
1703
1944
|
|
|
@@ -2123,6 +2364,96 @@ def model_supports_tool_calls(model: Optional[str], provider: Optional[str]) ->
|
|
|
2123
2364
|
return any(marker in model_lower for marker in toolish_markers)
|
|
2124
2365
|
|
|
2125
2366
|
|
|
2367
|
+
def wrap_tool_with_display(tool_name: str, tool_func: Callable, state: ShellState) -> Callable:
|
|
2368
|
+
"""Wrap a tool function to add visual feedback when it executes.
|
|
2369
|
+
|
|
2370
|
+
Respects state.log_level:
|
|
2371
|
+
- "silent": no output
|
|
2372
|
+
- "normal": show tool name and success/failure
|
|
2373
|
+
- "verbose": show tool name, args, success/failure, and result preview
|
|
2374
|
+
"""
|
|
2375
|
+
def wrapped(**kwargs):
|
|
2376
|
+
log_level = getattr(state, 'log_level', 'normal')
|
|
2377
|
+
|
|
2378
|
+
# Display tool call (skip in silent mode)
|
|
2379
|
+
if log_level != "silent":
|
|
2380
|
+
try:
|
|
2381
|
+
args_display = ""
|
|
2382
|
+
# Always show a preview of args for key tools
|
|
2383
|
+
if kwargs:
|
|
2384
|
+
# For sh/python/sql, show the code/command being run
|
|
2385
|
+
if tool_name in ('sh', 'python', 'sql', 'cmd') and 'code' in kwargs:
|
|
2386
|
+
code_preview = str(kwargs['code']).strip().split('\n')[0] # First line
|
|
2387
|
+
if len(code_preview) > 80:
|
|
2388
|
+
code_preview = code_preview[:80] + "…"
|
|
2389
|
+
args_display = code_preview
|
|
2390
|
+
elif tool_name == 'sh' and 'bash_command' in kwargs:
|
|
2391
|
+
cmd_preview = str(kwargs['bash_command']).strip().split('\n')[0]
|
|
2392
|
+
if len(cmd_preview) > 80:
|
|
2393
|
+
cmd_preview = cmd_preview[:80] + "…"
|
|
2394
|
+
args_display = cmd_preview
|
|
2395
|
+
elif tool_name in ('sh', 'cmd') and 'command' in kwargs:
|
|
2396
|
+
cmd_preview = str(kwargs['command']).strip().split('\n')[0]
|
|
2397
|
+
if len(cmd_preview) > 80:
|
|
2398
|
+
cmd_preview = cmd_preview[:80] + "…"
|
|
2399
|
+
args_display = cmd_preview
|
|
2400
|
+
elif tool_name == 'python' and 'python_code' in kwargs:
|
|
2401
|
+
code_preview = str(kwargs['python_code']).strip().split('\n')[0]
|
|
2402
|
+
if len(code_preview) > 80:
|
|
2403
|
+
code_preview = code_preview[:80] + "…"
|
|
2404
|
+
args_display = code_preview
|
|
2405
|
+
elif tool_name == 'agent' and 'npc_name' in kwargs:
|
|
2406
|
+
args_display = f"@{kwargs['npc_name']}"
|
|
2407
|
+
if 'request' in kwargs:
|
|
2408
|
+
req = str(kwargs['request'])[:50]
|
|
2409
|
+
args_display += f": {req}…" if len(str(kwargs['request'])) > 50 else f": {req}"
|
|
2410
|
+
elif tool_name == 'agent' and 'query' in kwargs:
|
|
2411
|
+
query_preview = str(kwargs['query']).strip()[:60]
|
|
2412
|
+
args_display = query_preview + ("…" if len(str(kwargs['query'])) > 60 else "")
|
|
2413
|
+
elif log_level == "verbose":
|
|
2414
|
+
arg_parts = []
|
|
2415
|
+
for _, v in kwargs.items():
|
|
2416
|
+
v_str = str(v)
|
|
2417
|
+
if len(v_str) > 40:
|
|
2418
|
+
v_str = v_str[:40] + "…"
|
|
2419
|
+
arg_parts.append(f"{v_str}")
|
|
2420
|
+
args_display = " ".join(arg_parts)
|
|
2421
|
+
if len(args_display) > 60:
|
|
2422
|
+
args_display = args_display[:60] + "…"
|
|
2423
|
+
|
|
2424
|
+
if args_display:
|
|
2425
|
+
print(colored(f" ⚡ {tool_name}", "cyan") + colored(f" {args_display}", "white", attrs=["dark"]), end="", flush=True)
|
|
2426
|
+
else:
|
|
2427
|
+
print(colored(f" ⚡ {tool_name}", "cyan"), end="", flush=True)
|
|
2428
|
+
except:
|
|
2429
|
+
pass
|
|
2430
|
+
|
|
2431
|
+
# Execute tool
|
|
2432
|
+
try:
|
|
2433
|
+
result = tool_func(**kwargs)
|
|
2434
|
+
if log_level != "silent":
|
|
2435
|
+
try:
|
|
2436
|
+
print(colored(" ✓", "green"), flush=True)
|
|
2437
|
+
# Show preview of result only in verbose mode
|
|
2438
|
+
if log_level == "verbose":
|
|
2439
|
+
result_preview = str(result)
|
|
2440
|
+
if len(result_preview) > 200:
|
|
2441
|
+
result_preview = result_preview[:200] + "..."
|
|
2442
|
+
if result_preview and result_preview not in ('None', '', '{}', '[]'):
|
|
2443
|
+
print(colored(f" → {result_preview}", "white", attrs=["dark"]), flush=True)
|
|
2444
|
+
except:
|
|
2445
|
+
pass
|
|
2446
|
+
return result
|
|
2447
|
+
except Exception as e:
|
|
2448
|
+
if log_level != "silent":
|
|
2449
|
+
try:
|
|
2450
|
+
print(colored(f" ✗ {str(e)[:100]}", "red"), flush=True)
|
|
2451
|
+
except:
|
|
2452
|
+
pass
|
|
2453
|
+
raise
|
|
2454
|
+
return wrapped
|
|
2455
|
+
|
|
2456
|
+
|
|
2126
2457
|
def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str, Callable]]:
|
|
2127
2458
|
"""
|
|
2128
2459
|
Assemble tool definitions + executable map from NPC tools, Jinxs, and MCP servers.
|
|
@@ -2230,7 +2561,11 @@ def collect_llm_tools(state: ShellState) -> Tuple[List[Dict[str, Any]], Dict[str
|
|
|
2230
2561
|
name = tool_def.get("function", {}).get("name")
|
|
2231
2562
|
if name:
|
|
2232
2563
|
deduped[name] = tool_def
|
|
2233
|
-
|
|
2564
|
+
|
|
2565
|
+
# Wrap all tools with display feedback for npcsh
|
|
2566
|
+
wrapped_tool_map = {name: wrap_tool_with_display(name, func, state) for name, func in tool_map.items()}
|
|
2567
|
+
|
|
2568
|
+
return list(deduped.values()), wrapped_tool_map
|
|
2234
2569
|
|
|
2235
2570
|
|
|
2236
2571
|
def should_skip_kg_processing(user_input: str, assistant_output: str) -> bool:
|
|
@@ -2454,15 +2789,19 @@ def process_pipeline_command(
|
|
|
2454
2789
|
else cmd_to_process
|
|
2455
2790
|
)
|
|
2456
2791
|
path_cmd = 'The current working directory is: ' + state.current_path
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2792
|
+
if os.path.exists(state.current_path):
|
|
2793
|
+
all_files = os.listdir(state.current_path)
|
|
2794
|
+
# Limit to first 100 files to avoid token explosion
|
|
2795
|
+
limited_files = all_files[:100]
|
|
2796
|
+
file_list = "\n".join([
|
|
2797
|
+
os.path.join(state.current_path, f)
|
|
2798
|
+
for f in limited_files
|
|
2799
|
+
])
|
|
2800
|
+
if len(all_files) > 100:
|
|
2801
|
+
file_list += f"\n... and {len(all_files) - 100} more files"
|
|
2802
|
+
ls_files = 'Files in the current directory (full paths):\n' + file_list
|
|
2803
|
+
else:
|
|
2804
|
+
ls_files = 'No files found in the current directory.'
|
|
2466
2805
|
platform_info = (
|
|
2467
2806
|
f"Platform: {platform.system()} {platform.release()} "
|
|
2468
2807
|
f"({platform.machine()})"
|
|
@@ -2524,21 +2863,68 @@ def process_pipeline_command(
|
|
|
2524
2863
|
# Only add tool_choice for providers that support it (not gemini)
|
|
2525
2864
|
is_gemini = (exec_provider and "gemini" in exec_provider.lower()) or \
|
|
2526
2865
|
(exec_model and "gemini" in exec_model.lower())
|
|
2527
|
-
|
|
2528
|
-
|
|
2866
|
+
llm_kwargs["tool_choice"] = 'auto'
|
|
2867
|
+
|
|
2868
|
+
# Agent loop: keep calling LLM until it stops making tool calls
|
|
2869
|
+
# The LLM decides when it's done - npcsh just facilitates
|
|
2870
|
+
iteration = 0
|
|
2871
|
+
max_iterations = 50 # Safety limit to prevent infinite loops
|
|
2872
|
+
total_usage = {"input_tokens": 0, "output_tokens": 0}
|
|
2873
|
+
|
|
2874
|
+
while iteration < max_iterations:
|
|
2875
|
+
iteration += 1
|
|
2876
|
+
|
|
2877
|
+
llm_result = get_llm_response(
|
|
2878
|
+
full_llm_cmd if iteration == 1 else None, # Only pass prompt on first call
|
|
2879
|
+
model=exec_model,
|
|
2880
|
+
provider=exec_provider,
|
|
2881
|
+
npc=state.npc,
|
|
2882
|
+
team=state.team,
|
|
2883
|
+
messages=state.messages,
|
|
2884
|
+
stream=False, # Don't stream intermediate calls
|
|
2885
|
+
attachments=state.attachments if iteration == 1 else None,
|
|
2886
|
+
context=info if iteration == 1 else None,
|
|
2887
|
+
**llm_kwargs,
|
|
2888
|
+
)
|
|
2889
|
+
|
|
2890
|
+
# Accumulate usage
|
|
2891
|
+
if isinstance(llm_result, dict) and llm_result.get('usage'):
|
|
2892
|
+
total_usage["input_tokens"] += llm_result['usage'].get('input_tokens', 0)
|
|
2893
|
+
total_usage["output_tokens"] += llm_result['usage'].get('output_tokens', 0)
|
|
2894
|
+
|
|
2895
|
+
# Update state messages
|
|
2896
|
+
old_msg_count = len(state.messages) if state.messages else 0
|
|
2897
|
+
if isinstance(llm_result, dict):
|
|
2898
|
+
state.messages = llm_result.get("messages", state.messages)
|
|
2899
|
+
|
|
2900
|
+
# Display tool outputs from this iteration
|
|
2901
|
+
for msg in state.messages[old_msg_count:]:
|
|
2902
|
+
if msg.get("role") == "tool":
|
|
2903
|
+
tool_name = msg.get("name", "tool")
|
|
2904
|
+
tool_content = msg.get("content", "")
|
|
2905
|
+
if tool_content and tool_content.strip():
|
|
2906
|
+
print(colored(f"\n⚡ {tool_name}:", "cyan"))
|
|
2907
|
+
lines = tool_content.split('\n')
|
|
2908
|
+
if len(lines) > 50:
|
|
2909
|
+
print('\n'.join(lines[:25]))
|
|
2910
|
+
print(colored(f"\n... ({len(lines) - 50} lines hidden) ...\n", "white", attrs=["dark"]))
|
|
2911
|
+
print('\n'.join(lines[-25:]))
|
|
2912
|
+
else:
|
|
2913
|
+
print(tool_content)
|
|
2914
|
+
|
|
2915
|
+
# Check if LLM made tool calls - if not, it's done
|
|
2916
|
+
tool_calls_made = isinstance(llm_result, dict) and llm_result.get("tool_calls")
|
|
2917
|
+
if not tool_calls_made:
|
|
2918
|
+
# LLM is done - no more tool calls
|
|
2919
|
+
break
|
|
2920
|
+
|
|
2921
|
+
# Clear the prompt for continuation calls - context is in messages
|
|
2922
|
+
full_llm_cmd = None
|
|
2923
|
+
|
|
2924
|
+
# Store accumulated usage
|
|
2925
|
+
if isinstance(llm_result, dict):
|
|
2926
|
+
llm_result['usage'] = total_usage
|
|
2529
2927
|
|
|
2530
|
-
llm_result = get_llm_response(
|
|
2531
|
-
full_llm_cmd,
|
|
2532
|
-
model=exec_model,
|
|
2533
|
-
provider=exec_provider,
|
|
2534
|
-
npc=state.npc,
|
|
2535
|
-
team=state.team,
|
|
2536
|
-
messages=state.messages,
|
|
2537
|
-
stream=stream_final,
|
|
2538
|
-
attachments=state.attachments,
|
|
2539
|
-
context=info,
|
|
2540
|
-
**llm_kwargs,
|
|
2541
|
-
)
|
|
2542
2928
|
else:
|
|
2543
2929
|
llm_result = check_llm_command(
|
|
2544
2930
|
full_llm_cmd,
|
|
@@ -2552,7 +2938,8 @@ def process_pipeline_command(
|
|
|
2552
2938
|
images=state.attachments,
|
|
2553
2939
|
stream=stream_final,
|
|
2554
2940
|
context=info,
|
|
2555
|
-
extra_globals=application_globals_for_jinx
|
|
2941
|
+
extra_globals=application_globals_for_jinx,
|
|
2942
|
+
tool_capable=tool_capable,
|
|
2556
2943
|
)
|
|
2557
2944
|
except KeyboardInterrupt:
|
|
2558
2945
|
print(colored("\nLLM processing interrupted by user.", "yellow"))
|
|
@@ -2781,6 +3168,11 @@ def execute_command(
|
|
|
2781
3168
|
if not command.strip():
|
|
2782
3169
|
return state, ""
|
|
2783
3170
|
|
|
3171
|
+
# Unescape @ and / at start of command that were escaped to prevent misinterpretation
|
|
3172
|
+
# (e.g., from pasted content that starts with @ or /)
|
|
3173
|
+
if command.startswith('\\@') or command.startswith('\\/'):
|
|
3174
|
+
command = command[1:] # Remove the escape backslash
|
|
3175
|
+
|
|
2784
3176
|
# Check for mode switch commands
|
|
2785
3177
|
mode_change, state = check_mode_switch(command, state)
|
|
2786
3178
|
if mode_change:
|
|
@@ -2802,6 +3194,8 @@ def execute_command(
|
|
|
2802
3194
|
|
|
2803
3195
|
original_command_for_embedding = command
|
|
2804
3196
|
commands = split_by_pipes(command)
|
|
3197
|
+
# Unescape pipe characters that were escaped to prevent splitting (e.g., from pasted content)
|
|
3198
|
+
commands = [cmd.replace('\\|', '|') for cmd in commands]
|
|
2805
3199
|
|
|
2806
3200
|
stdin_for_next = None
|
|
2807
3201
|
final_output = None
|
|
@@ -2993,32 +3387,44 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
2993
3387
|
default_forenpc_name = "forenpc"
|
|
2994
3388
|
else:
|
|
2995
3389
|
if not os.path.exists('.npcsh_global'):
|
|
2996
|
-
|
|
3390
|
+
try:
|
|
3391
|
+
resp = input(f"No npc_team found in {os.getcwd()}. Create a new team here? [Y/n]: ").strip().lower()
|
|
3392
|
+
except (KeyboardInterrupt, EOFError):
|
|
3393
|
+
print("\nAborted.")
|
|
3394
|
+
sys.exit(0)
|
|
2997
3395
|
if resp in ("", "y", "yes"):
|
|
2998
3396
|
team_dir = project_team_path
|
|
2999
3397
|
os.makedirs(team_dir, exist_ok=True)
|
|
3000
3398
|
default_forenpc_name = "forenpc"
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3399
|
+
try:
|
|
3400
|
+
forenpc_directive = input(
|
|
3401
|
+
f"Enter a primary directive for {default_forenpc_name} (default: 'You are the forenpc of the team...'): "
|
|
3402
|
+
).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."
|
|
3403
|
+
forenpc_model = input("Enter a model for your forenpc (default: llama3.2): ").strip() or "llama3.2"
|
|
3404
|
+
forenpc_provider = input("Enter a provider for your forenpc (default: ollama): ").strip() or "ollama"
|
|
3405
|
+
except (KeyboardInterrupt, EOFError):
|
|
3406
|
+
print("\nAborted.")
|
|
3407
|
+
sys.exit(0)
|
|
3408
|
+
|
|
3007
3409
|
with open(os.path.join(team_dir, f"{default_forenpc_name}.npc"), "w") as f:
|
|
3008
3410
|
yaml.dump({
|
|
3009
3411
|
"name": default_forenpc_name, "primary_directive": forenpc_directive,
|
|
3010
3412
|
"model": forenpc_model, "provider": forenpc_provider
|
|
3011
3413
|
}, f)
|
|
3012
|
-
|
|
3414
|
+
|
|
3013
3415
|
ctx_path = os.path.join(team_dir, "team.ctx")
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3416
|
+
try:
|
|
3417
|
+
folder_context = input("Enter a short description for this project/team (optional): ").strip()
|
|
3418
|
+
team_ctx_data = {
|
|
3419
|
+
"forenpc": default_forenpc_name,
|
|
3420
|
+
"model": forenpc_model,
|
|
3421
|
+
"provider": forenpc_provider,
|
|
3422
|
+
"context": folder_context if folder_context else None
|
|
3423
|
+
}
|
|
3424
|
+
use_jinxs = input("Use global jinxs folder (g) or copy to this project (c)? [g/c, default: g]: ").strip().lower()
|
|
3425
|
+
except (KeyboardInterrupt, EOFError):
|
|
3426
|
+
print("\nAborted.")
|
|
3427
|
+
sys.exit(0)
|
|
3022
3428
|
if use_jinxs == "c":
|
|
3023
3429
|
global_jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
|
|
3024
3430
|
if os.path.exists(global_jinxs_dir):
|
|
@@ -3035,7 +3441,7 @@ def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
|
|
|
3035
3441
|
with open(".npcsh_global", "w") as f:
|
|
3036
3442
|
pass
|
|
3037
3443
|
team_dir = global_team_path
|
|
3038
|
-
default_forenpc_name = "sibiji"
|
|
3444
|
+
default_forenpc_name = "sibiji"
|
|
3039
3445
|
else:
|
|
3040
3446
|
team_dir = global_team_path
|
|
3041
3447
|
default_forenpc_name = "sibiji"
|
|
@@ -3195,7 +3601,8 @@ def process_result(
|
|
|
3195
3601
|
|
|
3196
3602
|
# FIX: Handle dict output properly
|
|
3197
3603
|
if isinstance(output, dict):
|
|
3198
|
-
|
|
3604
|
+
# Use None-safe check to not skip empty strings
|
|
3605
|
+
output_content = output.get('output') if 'output' in output else output.get('response')
|
|
3199
3606
|
model_for_stream = output.get('model', active_npc.model)
|
|
3200
3607
|
provider_for_stream = output.get('provider', active_npc.provider)
|
|
3201
3608
|
|
|
@@ -3212,18 +3619,22 @@ def process_result(
|
|
|
3212
3619
|
usage.get('output_tokens', 0)
|
|
3213
3620
|
)
|
|
3214
3621
|
|
|
3215
|
-
# If output_content is still a dict
|
|
3622
|
+
# If output_content is still a dict, convert to string
|
|
3216
3623
|
if isinstance(output_content, dict):
|
|
3217
3624
|
output_content = str(output_content)
|
|
3218
|
-
elif output_content is None:
|
|
3219
|
-
|
|
3625
|
+
elif output_content is None or output_content == '':
|
|
3626
|
+
# No output from the agent - this is fine, don't show annoying message
|
|
3627
|
+
output_content = None
|
|
3220
3628
|
else:
|
|
3221
3629
|
output_content = output
|
|
3222
3630
|
model_for_stream = active_npc.model
|
|
3223
3631
|
provider_for_stream = active_npc.provider
|
|
3224
3632
|
|
|
3225
3633
|
print('\n')
|
|
3226
|
-
if
|
|
3634
|
+
if output_content is None:
|
|
3635
|
+
# No output to display - tool results already shown during execution
|
|
3636
|
+
pass
|
|
3637
|
+
elif user_input == '/help':
|
|
3227
3638
|
if isinstance(output_content, str):
|
|
3228
3639
|
render_markdown(output_content)
|
|
3229
3640
|
else:
|
|
@@ -3235,12 +3646,12 @@ def process_result(
|
|
|
3235
3646
|
render_markdown(final_output_str)
|
|
3236
3647
|
else:
|
|
3237
3648
|
final_output_str = print_and_process_stream_with_markdown(
|
|
3238
|
-
output_content,
|
|
3239
|
-
model_for_stream,
|
|
3240
|
-
provider_for_stream,
|
|
3649
|
+
output_content,
|
|
3650
|
+
model_for_stream,
|
|
3651
|
+
provider_for_stream,
|
|
3241
3652
|
show=True
|
|
3242
3653
|
)
|
|
3243
|
-
|
|
3654
|
+
else:
|
|
3244
3655
|
final_output_str = str(output_content)
|
|
3245
3656
|
render_markdown(final_output_str)
|
|
3246
3657
|
|