npcsh 1.0.14__py3-none-any.whl → 1.0.17__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/routes.py CHANGED
@@ -3,6 +3,10 @@
3
3
  from typing import Callable, Dict, Any, List, Optional, Union
4
4
  import functools
5
5
  import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
6
10
  import traceback
7
11
  import shlex
8
12
  import time
@@ -18,7 +22,6 @@ from npcpy.llm_funcs import (
18
22
  gen_video,
19
23
  breathe,
20
24
  )
21
- from npcpy.npc_compiler import NPC, Team, Jinx
22
25
  from npcpy.npc_compiler import initialize_npc_project
23
26
  from npcpy.npc_sysenv import render_markdown
24
27
  from npcpy.work.plan import execute_plan_command
@@ -29,8 +32,7 @@ from npcpy.memory.command_history import CommandHistory, load_kg_from_db, save_k
29
32
  from npcpy.serve import start_flask_server
30
33
  from npcpy.mix.debate import run_debate
31
34
  from npcpy.data.image import capture_screenshot
32
- from npcpy.npc_compiler import NPC, Team, Jinx
33
- from npcpy.npc_compiler import initialize_npc_project
35
+ from npcpy.npc_compiler import NPC, Team, Jinx,initialize_npc_project
34
36
  from npcpy.data.web import search_web
35
37
  from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process
36
38
 
@@ -38,6 +40,7 @@ from npcpy.memory.knowledge_graph import kg_sleep_process, kg_dream_process
38
40
  from npcsh._state import (
39
41
  NPCSH_VISION_MODEL,
40
42
  NPCSH_VISION_PROVIDER,
43
+ set_npcsh_config_value,
41
44
  NPCSH_API_URL,
42
45
  NPCSH_CHAT_MODEL,
43
46
  NPCSH_CHAT_PROVIDER,
@@ -55,15 +58,20 @@ from npcsh._state import (
55
58
  normalize_and_expand_flags,
56
59
  get_argument_help
57
60
  )
61
+ from npcsh.corca import enter_corca_mode
58
62
  from npcsh.guac import enter_guac_mode
59
63
  from npcsh.plonk import execute_plonk_command, format_plonk_summary
60
64
  from npcsh.alicanto import alicanto
65
+ from npcsh.pti import enter_pti_mode
61
66
  from npcsh.spool import enter_spool_mode
62
67
  from npcsh.wander import enter_wander_mode
63
68
  from npcsh.yap import enter_yap_mode
64
69
 
65
70
 
66
71
 
72
+ NPC_STUDIO_DIR = Path.home() / ".npcsh" / "npc-studio"
73
+
74
+
67
75
  class CommandRouter:
68
76
  def __init__(self):
69
77
  self.routes = {}
@@ -222,6 +230,10 @@ def compile_handler(command: str, **kwargs):
222
230
 
223
231
 
224
232
 
233
+ @router.route("corca", "Enter the Corca MCP-powered agentic shell. Usage: /corca [--mcp-server-path path]")
234
+ def corca_handler(command: str, **kwargs):
235
+ return enter_corca_mode(command=command, **kwargs)
236
+
225
237
  @router.route("flush", "Flush the last N messages")
226
238
  def flush_handler(command: str, **kwargs):
227
239
  messages = safe_get(kwargs, "messages", [])
@@ -279,7 +291,8 @@ def guac_handler(command, **kwargs):
279
291
  team = Team(npc_team_dir, db_conn=db_conn)
280
292
 
281
293
 
282
- enter_guac_mode(npc=npc,
294
+ enter_guac_mode(workspace_dirs,
295
+ npc=npc,
283
296
  team=team,
284
297
  config_dir=config_dir,
285
298
  plots_dir=plots_dir,
@@ -370,9 +383,77 @@ def init_handler(command: str, **kwargs):
370
383
  output = f"Error initializing project: {e}"
371
384
  return {"output": output, "messages": messages}
372
385
 
386
+ def ensure_repo():
387
+ """Clone or update the npc-studio repo."""
388
+ if not NPC_STUDIO_DIR.exists():
389
+ os.makedirs(NPC_STUDIO_DIR.parent, exist_ok=True)
390
+ subprocess.check_call([
391
+ "git", "clone",
392
+ "https://github.com/npc-worldwide/npc-studio.git",
393
+ str(NPC_STUDIO_DIR)
394
+ ])
395
+ else:
396
+ subprocess.check_call(
397
+ ["git", "pull"],
398
+ cwd=NPC_STUDIO_DIR
399
+ )
373
400
 
401
+ def install_dependencies():
402
+ """Install npm and pip dependencies."""
403
+ # Install frontend deps
404
+ subprocess.check_call(["npm", "install"], cwd=NPC_STUDIO_DIR)
374
405
 
406
+ # Install backend deps
407
+ req_file = NPC_STUDIO_DIR / "requirements.txt"
408
+ if req_file.exists():
409
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", str(req_file)])
410
+ def launch_npc_studio(path_to_open: str = None):
411
+ """
412
+ Launch the NPC Studio backend + frontend.
413
+ Returns PIDs for processes.
414
+ """
415
+ ensure_repo()
416
+ install_dependencies()
417
+
418
+ # Start backend (Flask server)
419
+ backend = subprocess.Popen(
420
+ [sys.executable, "npc_studio_serve.py"],
421
+ cwd=NPC_STUDIO_DIR,
422
+ shell = False
423
+ )
424
+
425
+ # Start server (Electron)
426
+ dev_server = subprocess.Popen(
427
+ ["npm", "run", "dev"],
428
+ cwd=NPC_STUDIO_DIR,
429
+ shell=False
430
+ )
431
+
432
+ # Start frontend (Electron)
433
+ frontend = subprocess.Popen(
434
+ ["npm", "start"],
435
+ cwd=NPC_STUDIO_DIR,
436
+ shell=False
437
+ )
438
+
439
+ return backend, dev_server, frontend
440
+ # ========== Router handler ==========
441
+ @router.route("npc-studio", "Start npc studio")
442
+ def npc_studio_handler(command: str, **kwargs):
443
+ messages = kwargs.get("messages", [])
444
+ user_command = " ".join(command.split()[1:])
375
445
 
446
+ try:
447
+ backend, electron, frontend = launch_npc_studio(user_command or None)
448
+ return {
449
+ "output": f"NPC Studio started!\nBackend PID={backend.pid}, Electron PID={electron.pid} Frontend PID={frontend.pid}",
450
+ "messages": messages
451
+ }
452
+ except Exception as e:
453
+ return {
454
+ "output": f"Failed to start NPC Studio: {e}",
455
+ "messages": messages
456
+ }
376
457
  @router.route("ots", "Take screenshot and analyze with vision model")
377
458
  def ots_handler(command: str, **kwargs):
378
459
  command_parts = command.split()
@@ -440,23 +521,25 @@ def ots_handler(command: str, **kwargs):
440
521
  return {"output": f"Error during /ots command: {e}", "messages": messages}
441
522
 
442
523
 
524
+
525
+
443
526
  @router.route("plan", "Execute a plan command")
444
527
  def plan_handler(command: str, **kwargs):
445
528
  messages = safe_get(kwargs, "messages", [])
446
529
  user_command = " ".join(command.split()[1:])
447
530
  if not user_command:
448
531
  return {"output": "Usage: /plan <description_of_plan>", "messages": messages}
449
- try:
450
- return execute_plan_command(command=user_command, **kwargs)
451
- except NameError:
452
- return {"output": "Plan function (execute_plan_command) not available.", "messages": messages}
453
- except Exception as e:
454
- traceback.print_exc()
455
- return {"output": f"Error executing plan: {e}", "messages": messages}
532
+ #try:
533
+ return execute_plan_command(command=user_command, **kwargs)
456
534
 
457
- @router.route("pti", "Use pardon-the-interruption mode to interact with the LLM")
535
+ #return {"output": "Plan function (execute_plan_command) not available.", "messages": messages}
536
+ #except Exception as e:
537
+ # traceback.print_exc()
538
+ # return {"output": f"Error executing plan: {e}", "messages": messages}
539
+
540
+ @router.route("pti", "Enter Pardon-The-Interruption mode for human-in-the-loop reasoning.")
458
541
  def pti_handler(command: str, **kwargs):
459
- return
542
+ return enter_pti_mode(command=command, **kwargs)
460
543
 
461
544
  @router.route("plonk", "Use vision model to interact with GUI. Usage: /plonk <task description>")
462
545
  def plonk_handler(command: str, **kwargs):
@@ -500,17 +583,18 @@ def brainblast_handler(command: str, **kwargs):
500
583
  parts = shlex.split(command)
501
584
  search_query = " ".join(parts[1:]) if len(parts) > 1 else ""
502
585
 
503
-
504
586
  if not search_query:
505
587
  return {"output": "Usage: /brainblast <search_terms>", "messages": messages}
506
588
 
507
589
  # Get the command history instance
508
590
  command_history = kwargs.get('command_history')
509
591
  if not command_history:
592
+ #print('no command history provided to brainblast')
510
593
  # Create a new one if not provided
511
594
  db_path = safe_get(kwargs, "history_db_path", os.path.expanduser('~/npcsh_history.db'))
512
595
  try:
513
596
  command_history = CommandHistory(db_path)
597
+ kwargs['command_history'] = command_history
514
598
  except Exception as e:
515
599
  return {"output": f"Error connecting to command history: {e}", "messages": messages}
516
600
 
@@ -521,13 +605,8 @@ def brainblast_handler(command: str, **kwargs):
521
605
 
522
606
  # Execute the brainblast command
523
607
  return execute_brainblast_command(
524
- command=search_query,
525
- command_history=command_history,
526
- messages=messages,
527
- top_k=safe_get(kwargs, 'top_k', 5),
528
- **kwargs
529
- )
530
-
608
+ command=search_query,
609
+ **kwargs)
531
610
  except Exception as e:
532
611
  traceback.print_exc()
533
612
  return {"output": f"Error executing brainblast command: {e}", "messages": messages}
@@ -831,29 +910,18 @@ def sleep_handler(command: str, **kwargs):
831
910
  @router.route("spool", "Enter interactive chat (spool) mode")
832
911
  def spool_handler(command: str, **kwargs):
833
912
  try:
834
- # Handle NPC loading if npc is passed as a string (name)
835
913
  npc = safe_get(kwargs, 'npc')
836
914
  team = safe_get(kwargs, 'team')
837
915
 
838
- # If npc is a string, try to load it from the team
839
916
  if isinstance(npc, str) and team:
840
917
  npc_name = npc
841
918
  if npc_name in team.npcs:
842
919
  npc = team.npcs[npc_name]
843
920
  else:
844
921
  return {"output": f"Error: NPC '{npc_name}' not found in team. Available NPCs: {', '.join(team.npcs.keys())}", "messages": safe_get(kwargs, "messages", [])}
845
-
846
- return enter_spool_mode(
847
- model=safe_get(kwargs, 'model', NPCSH_CHAT_MODEL),
848
- provider=safe_get(kwargs, 'provider', NPCSH_CHAT_PROVIDER),
849
- npc=npc,
850
- team=team,
851
- messages=safe_get(kwargs, 'messages'),
852
- conversation_id=safe_get(kwargs, 'conversation_id'),
853
- stream=safe_get(kwargs, 'stream', NPCSH_STREAM_OUTPUT),
854
- attachments=safe_get(kwargs, 'attachments'),
855
- rag_similarity_threshold = safe_get(kwargs, 'rag_similarity_threshold', 0.3),
856
- )
922
+ kwargs['npc'] = npc
923
+ return enter_spool_mode(
924
+ **kwargs)
857
925
  except Exception as e:
858
926
  traceback.print_exc()
859
927
  return {"output": f"Error entering spool mode: {e}", "messages": safe_get(kwargs, "messages", [])}
@@ -913,13 +981,15 @@ def vixynt_handler(command: str, **kwargs):
913
981
  width = safe_get(kwargs, 'width', 1024)
914
982
  output_file = safe_get(kwargs, 'output_file')
915
983
  attachments = safe_get(kwargs, 'attachments')
984
+ if isinstance(attachments, str):
985
+ attachments = attachments.split(',')
986
+
916
987
  messages = safe_get(kwargs, 'messages', [])
917
988
 
918
989
  user_prompt = " ".join(safe_get(kwargs, 'positional_args', []))
919
990
 
920
991
  if not user_prompt:
921
992
  return {"output": "Usage: /vixynt <prompt> [--output_file path] [--attachments path]", "messages": messages}
922
-
923
993
  try:
924
994
  image = gen_image(
925
995
  prompt=user_prompt,
npcsh/spool.py CHANGED
@@ -4,174 +4,126 @@ from npcpy.data.image import capture_screenshot
4
4
  from npcpy.data.text import rag_search
5
5
 
6
6
  import os
7
+ import sys
7
8
  from npcpy.npc_sysenv import (
8
9
  print_and_process_stream_with_markdown,
10
+ get_system_message,
11
+ render_markdown,
9
12
  )
10
- from npcpy.npc_sysenv import (
11
- get_system_message,
12
- render_markdown,
13
-
14
- )
15
- from npcsh._state import (
13
+ from npcsh._state import (
16
14
  orange,
17
- NPCSH_VISION_MODEL,
18
- NPCSH_VISION_PROVIDER,
19
- NPCSH_CHAT_MODEL,
20
- NPCSH_CHAT_PROVIDER,
21
- NPCSH_STREAM_OUTPUT
15
+ ShellState,
16
+ execute_command,
17
+ get_multiline_input,
18
+ readline_safe_prompt,
19
+ setup_shell,
20
+ get_npc_path,
21
+ process_result,
22
+ initial_state,
22
23
  )
23
- from npcpy.llm_funcs import (get_llm_response,)
24
-
24
+ from npcpy.llm_funcs import get_llm_response
25
25
  from npcpy.npc_compiler import NPC
26
26
  from typing import Any, List, Dict, Union
27
27
  from npcsh.yap import enter_yap_mode
28
-
29
-
28
+ from termcolor import colored
29
+ def print_spool_ascii():
30
+ spool_art = """
31
+ ██████╗██████╗ ████████╗ ████████╗ ██╗
32
+ ██╔════╝██╔══██╗██╔🧵🧵🧵██ ██╔🧵🧵🧵██ ██║
33
+ ╚█████╗ ██████╔╝██║🧵🔴🧵██ ██║🧵🔴🧵██ ██║
34
+ ╚═══██╗██╔═══╝ ██║🧵🧵🧵██ ██║🧵🧵🧵██ ██║
35
+ ██████╔╝██║ ██╚══════██ ██ ══════██ ██║
36
+ ╚═════╝ ╚═╝ ╚═████████ ███████═╝ █████████╗
37
+ """
38
+ print(spool_art)
30
39
  def enter_spool_mode(
31
40
  npc: NPC = None,
32
41
  team = None,
33
42
  model: str = None,
34
43
  provider: str = None,
35
- vision_model:str = None,
36
- vision_provider:str = None,
44
+ vmodel: str = None,
45
+ vprovider: str = None,
37
46
  attachments: List[str] = None,
38
47
  rag_similarity_threshold: float = 0.3,
39
48
  messages: List[Dict] = None,
40
49
  conversation_id: str = None,
41
- stream: bool = NPCSH_STREAM_OUTPUT,
50
+ stream: bool = None,
42
51
  **kwargs,
43
52
  ) -> Dict:
53
+ print_spool_ascii()
54
+ # Initialize state using existing infrastructure
55
+ command_history, state_team, default_npc = setup_shell()
44
56
 
45
- session_model = model or (npc.model if npc else NPCSH_CHAT_MODEL)
46
- session_provider = provider or (npc.provider if npc else NPCSH_CHAT_PROVIDER)
47
- session_vision_model = vision_model or NPCSH_VISION_MODEL
48
- session_vision_provider = vision_provider or NPCSH_VISION_PROVIDER
49
-
50
- npc_info = f" (NPC: {npc.name})" if npc else ""
51
- print(f"Entering spool mode{npc_info}. Type '/sq' to exit spool mode.")
57
+ # Create spool state, inheriting from initial_state
58
+ spool_state = ShellState(
59
+ npc=npc or default_npc,
60
+ team=team or state_team,
61
+ messages=messages.copy() if messages else [],
62
+ conversation_id=conversation_id or start_new_conversation(),
63
+ current_path=os.getcwd(),
64
+ stream_output=stream if stream is not None else initial_state.stream_output,
65
+ attachments=None,
66
+ )
67
+
68
+ # Override models/providers if specified
69
+ if model:
70
+ spool_state.chat_model = model
71
+ if provider:
72
+ spool_state.chat_provider = provider
73
+ if vmodel:
74
+ spool_state.vision_model = vmodel
75
+ if vprovider:
76
+ spool_state.vision_provider = vprovider
77
+
78
+ npc_info = f" (NPC: {spool_state.npc.name})" if spool_state.npc else ""
79
+ print(f"🧵 Entering spool mode{npc_info}. Type '/sq' to exit spool mode.")
52
80
  print("💡 Tip: Press Ctrl+C during streaming to interrupt and continue with a new message.")
53
81
 
54
- spool_context = messages.copy() if messages else []
82
+ # Handle file loading
55
83
  loaded_chunks = {}
56
-
57
- if not conversation_id:
58
- conversation_id = start_new_conversation()
59
-
60
- command_history = CommandHistory()
61
-
62
- files_to_load = attachments
63
- if files_to_load:
64
- if isinstance(files_to_load, str):
65
- files_to_load = [f.strip() for f in files_to_load.split(',')]
84
+ if attachments:
85
+ if isinstance(attachments, str):
86
+ attachments = [f.strip() for f in attachments.split(',')]
66
87
 
67
- for file_path in files_to_load:
88
+ for file_path in attachments:
68
89
  file_path = os.path.expanduser(file_path)
69
90
  if not os.path.exists(file_path):
70
- print(f"Error: File not found at {file_path}")
91
+ print(colored(f"Error: File not found at {file_path}", "red"))
71
92
  continue
72
93
  try:
73
94
  chunks = load_file_contents(file_path)
74
95
  loaded_chunks[file_path] = chunks
75
- print(f"Loaded {len(chunks)} chunks from: {file_path}")
96
+ print(colored(f"Loaded {len(chunks)} chunks from: {file_path}", "green"))
76
97
  except Exception as e:
77
- print(f"Error loading {file_path}: {str(e)}")
78
-
79
- system_message = get_system_message(npc) if npc else "You are a helpful assistant."
80
- if not spool_context or spool_context[0].get("role") != "system":
81
- spool_context.insert(0, {"role": "system", "content": system_message})
82
-
83
- if loaded_chunks:
84
- initial_file_context = "\n\n--- The user has loaded the following files for this session ---\n"
85
- for filename, chunks in loaded_chunks.items():
86
- initial_file_context += f"\n\n--- Start of content from {filename} ---\n"
87
- initial_file_context += "\n".join(chunks)
88
- initial_file_context += f"\n--- End of content from {filename} ---\n"
89
-
90
- def _handle_llm_interaction(
91
- prompt,
92
- current_context,
93
- model_to_use,
94
- provider_to_use,
95
- images_to_use=None
96
- ):
97
-
98
- current_context.append({"role": "user", "content": prompt})
98
+ print(colored(f"Error loading {file_path}: {str(e)}", "red"))
99
99
 
100
- save_conversation_message(
101
- command_history,
102
- conversation_id,
103
- "user",
104
- prompt,
105
- wd=os.getcwd(),
106
- model=model_to_use,
107
- provider=provider_to_use,
108
- npc=npc.name if npc else None,
109
- team=team.name if team else None,
110
- )
111
-
112
- assistant_reply = ""
113
-
114
- try:
115
- response = get_llm_response(
116
- prompt,
117
- model=model_to_use,
118
- provider=provider_to_use,
119
- messages=current_context,
120
- images=images_to_use,
121
- stream=stream,
122
- npc=npc
123
- )
124
- assistant_reply = response.get('response')
125
-
126
- if stream:
127
- print(orange(f'{npc.name if npc else "🧵"}....> '), end='', flush=True)
128
-
129
- # The streaming function now handles KeyboardInterrupt internally
130
- assistant_reply = print_and_process_stream_with_markdown(
131
- assistant_reply,
132
- model=model_to_use,
133
- provider=provider_to_use
134
- )
135
- else:
136
- render_markdown(assistant_reply)
137
-
138
- except Exception as e:
139
- assistant_reply = f"[Error during response generation: {str(e)}]"
140
- print(f"\n❌ Error: {str(e)}")
141
-
142
- current_context.append({"role": "assistant", "content": assistant_reply})
143
-
144
- if assistant_reply and assistant_reply.count("```") % 2 != 0:
145
- assistant_reply += "```"
146
-
147
- save_conversation_message(
148
- command_history,
149
- conversation_id,
150
- "assistant",
151
- assistant_reply,
152
- wd=os.getcwd(),
153
- model=model_to_use,
154
- provider=provider_to_use,
155
- npc=npc.name if npc else None,
156
- team=team.name if team else None,
157
- )
158
-
159
- return current_context
100
+ # Initialize context with system message if needed
101
+ if not spool_state.messages or spool_state.messages[0].get("role") != "system":
102
+ system_message = get_system_message(spool_state.npc) if spool_state.npc else "You are a helpful assistant."
103
+ spool_state.messages.insert(0, {"role": "system", "content": system_message})
160
104
 
161
105
  while True:
162
106
  try:
163
- prompt_text = orange(f"🧵:{npc.name if npc else 'chat'}:{session_model}> ")
164
- user_input = input(prompt_text).strip()
107
+ # Use consistent prompt styling with npcsh
108
+ npc_name = spool_state.npc.name if spool_state.npc else "chat"
109
+ display_model = spool_state.npc.model if spool_state.npc and spool_state.npc.model else spool_state.chat_model
110
+
111
+ prompt_str = f"{orange(npc_name)}:{display_model}🧵> "
112
+ prompt = readline_safe_prompt(prompt_str)
113
+ user_input = get_multiline_input(prompt).strip()
165
114
 
166
115
  if not user_input:
167
116
  continue
117
+
168
118
  if user_input.lower() == "/sq":
169
119
  print("Exiting spool mode.")
170
120
  break
121
+
171
122
  if user_input.lower() == "/yap":
172
- spool_context = enter_yap_mode(spool_context, npc)
123
+ spool_state.messages = enter_yap_mode(spool_state.messages, spool_state.npc)
173
124
  continue
174
125
 
126
+ # Handle vision commands
175
127
  if user_input.startswith("/ots"):
176
128
  command_parts = user_input.split()
177
129
  image_paths = []
@@ -179,26 +131,42 @@ def enter_spool_mode(
179
131
  if len(command_parts) > 1:
180
132
  for img_path in command_parts[1:]:
181
133
  full_path = os.path.expanduser(img_path)
182
- if os.path.exists(full_path): image_paths.append(full_path)
183
- else: print(f"Error: Image file not found at {full_path}")
134
+ if os.path.exists(full_path):
135
+ image_paths.append(full_path)
136
+ else:
137
+ print(colored(f"Error: Image file not found at {full_path}", "red"))
184
138
  else:
185
139
  screenshot = capture_screenshot()
186
140
  if screenshot and "file_path" in screenshot:
187
141
  image_paths.append(screenshot["file_path"])
188
- print(f"Screenshot captured: {screenshot['filename']}")
142
+ print(colored(f"Screenshot captured: {screenshot['filename']}", "green"))
189
143
 
190
- if not image_paths: continue
144
+ if not image_paths:
145
+ continue
191
146
 
192
147
  vision_prompt = input("Prompt for image(s) (or press Enter): ").strip() or "Describe these images."
193
- spool_context = _handle_llm_interaction(
194
- vision_prompt,
195
- spool_context,
196
- session_vision_model,
197
- session_vision_provider,
198
- images_to_use=image_paths
148
+
149
+ # Use vision models for image processing
150
+ response = get_llm_response(
151
+ vision_prompt,
152
+ model=spool_state.vision_model,
153
+ provider=spool_state.vision_provider,
154
+ messages=spool_state.messages,
155
+ images=image_paths,
156
+ stream=spool_state.stream_output,
157
+ npc=spool_state.npc,
158
+ **kwargs
159
+
199
160
  )
161
+
162
+ spool_state.messages = response.get('messages', spool_state.messages)
163
+ output = response.get('response')
164
+
165
+ # Process and display the result
166
+ process_result(vision_prompt, spool_state, {'output': output}, command_history)
200
167
  continue
201
168
 
169
+ # Handle RAG context if files are loaded
202
170
  current_prompt = user_input
203
171
  if loaded_chunks:
204
172
  context_content = ""
@@ -214,24 +182,32 @@ def enter_spool_mode(
214
182
 
215
183
  if context_content:
216
184
  current_prompt += f"\n\n--- Relevant context from loaded files ---\n{context_content}"
217
- print(f'prepped context_content : {context_content}')
218
185
 
219
- spool_context = _handle_llm_interaction(
220
- current_prompt,
221
- spool_context,
222
- session_model,
223
- session_provider
186
+ # Use standard LLM processing
187
+ response = get_llm_response(
188
+ current_prompt,
189
+ model=spool_state.npc.model if spool_state.npc and spool_state.npc.model else spool_state.chat_model,
190
+ provider=spool_state.npc.provider if spool_state.npc and spool_state.npc.provider else spool_state.chat_provider,
191
+ messages=spool_state.messages,
192
+ stream=spool_state.stream_output,
193
+ npc=spool_state.npc,
194
+ **kwargs
224
195
  )
196
+
197
+ spool_state.messages = response.get('messages', spool_state.messages)
198
+ output = response.get('response')
199
+
200
+ # Use existing result processing
201
+ process_result(current_prompt, spool_state, {'output': output}, command_history)
225
202
 
226
203
  except (EOFError,):
227
204
  print("\nExiting spool mode.")
228
205
  break
229
206
  except KeyboardInterrupt:
230
- # This handles Ctrl+C at the input prompt (not during streaming)
231
207
  print("\n🔄 Use '/sq' to exit or continue with a new message.")
232
208
  continue
233
209
 
234
- return {"messages": spool_context, "output": "Exited spool mode."}
210
+ return {"messages": spool_state.messages, "output": "Exited spool mode."}
235
211
 
236
212
 
237
213
  def main():
@@ -241,14 +217,32 @@ def main():
241
217
  parser.add_argument("--provider", help="Provider to use")
242
218
  parser.add_argument("--attachments", nargs="*", help="Files to load into context")
243
219
  parser.add_argument("--stream", default="true", help="Use streaming mode")
244
- parser.add_argument("--npc", type=str, default=os.path.expanduser('~/.npcsh/npc_team/sibiji.npc'), help="Path to NPC file")
220
+ parser.add_argument("--npc", type=str, help="NPC name or path to NPC file", default='sibiji',)
245
221
 
246
222
  args = parser.parse_args()
247
223
 
248
- npc = NPC(file=args.npc) if os.path.exists(os.path.expanduser(args.npc)) else None
224
+ # Use existing infrastructure to get NPC
225
+ command_history, team, default_npc = setup_shell()
226
+
227
+ npc = None
228
+ if args.npc:
229
+ if os.path.exists(os.path.expanduser(args.npc)):
230
+ npc = NPC(file=args.npc)
231
+ elif team and args.npc in team.npcs:
232
+ npc = team.npcs[args.npc]
233
+ else:
234
+ try:
235
+ npc_path = get_npc_path(args.npc, command_history.db_path)
236
+ npc = NPC(file=npc_path)
237
+ except ValueError:
238
+ print(colored(f"NPC '{args.npc}' not found. Using default.", "yellow"))
239
+ npc = default_npc
240
+ else:
241
+ npc = default_npc
249
242
 
250
243
  enter_spool_mode(
251
244
  npc=npc,
245
+ team=team,
252
246
  model=args.model,
253
247
  provider=args.provider,
254
248
  attachments=args.attachments,