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/npcsh.py CHANGED
@@ -1,1048 +1,55 @@
1
1
  import os
2
2
  import sys
3
- import atexit
4
- import subprocess
5
- import shlex
6
- import re
7
- from datetime import datetime
8
3
  import argparse
9
4
  import importlib.metadata
10
- import textwrap
11
- from typing import Optional, List, Dict, Any, Tuple, Union
12
- from dataclasses import dataclass, field
5
+
13
6
  import platform
14
7
  try:
15
8
  from termcolor import colored
16
9
  except:
17
10
  pass
18
-
19
- try:
20
- import chromadb
21
- except ImportError:
22
- chromadb = None
23
- import shutil
24
- import json
25
- import sqlite3
26
- import copy
27
- import yaml
28
-
29
- from npcsh._state import (
30
- setup_npcsh_config,
31
- initial_state,
32
- is_npcsh_initialized,
33
- initialize_base_npcs_if_needed,
34
- orange,
35
- ShellState,
36
- interactive_commands,
37
- BASH_COMMANDS,
38
-
39
- start_interactive_session,
40
- validate_bash_command,
41
- normalize_and_expand_flags,
42
-
43
- )
44
-
45
11
  from npcpy.npc_sysenv import (
46
- print_and_process_stream_with_markdown,
47
12
  render_markdown,
48
- get_locally_available_models,
49
- get_model_and_provider,
50
- lookup_provider
51
13
  )
52
- from npcsh.routes import router
53
- from npcpy.data.image import capture_screenshot
54
14
  from npcpy.memory.command_history import (
55
15
  CommandHistory,
56
- save_conversation_message,
57
16
  load_kg_from_db,
58
17
  save_kg_to_db,
59
18
  )
60
- from npcpy.npc_compiler import NPC, Team, load_jinxs_from_directory
61
- from npcpy.llm_funcs import (
62
- check_llm_command,
63
- get_llm_response,
64
- execute_llm_command,
65
- breathe
66
- )
19
+ from npcpy.npc_compiler import NPC
67
20
  from npcpy.memory.knowledge_graph import (
68
- kg_initial,
69
21
  kg_evolve_incremental
70
22
  )
71
- from npcpy.gen.embeddings import get_embeddings
72
23
 
24
+ from npcsh.routes import router
73
25
  try:
74
26
  import readline
75
27
  except:
76
28
  print('no readline support, some features may not work as desired. ')
77
- # --- Constants ---
29
+
78
30
  try:
79
- VERSION = importlib.metadata.version("npcpy")
31
+ VERSION = importlib.metadata.version("npcsh")
80
32
  except importlib.metadata.PackageNotFoundError:
81
33
  VERSION = "unknown"
82
34
 
83
- TERMINAL_EDITORS = ["vim", "emacs", "nano"]
84
- EMBEDDINGS_DB_PATH = os.path.expanduser("~/npcsh_chroma.db")
85
- HISTORY_DB_DEFAULT_PATH = os.path.expanduser("~/npcsh_history.db")
86
- READLINE_HISTORY_FILE = os.path.expanduser("~/.npcsh_readline_history")
87
- DEFAULT_NPC_TEAM_PATH = os.path.expanduser("~/.npcsh/npc_team/")
88
- PROJECT_NPC_TEAM_PATH = "./npc_team/"
89
-
90
- # --- Global Clients ---
91
- try:
92
- chroma_client = chromadb.PersistentClient(path=EMBEDDINGS_DB_PATH) if chromadb else None
93
- except Exception as e:
94
- print(f"Warning: Failed to initialize ChromaDB client at {EMBEDDINGS_DB_PATH}: {e}")
95
- chroma_client = None
96
-
97
-
98
-
99
-
100
- def get_path_executables() -> List[str]:
101
- """Get executables from PATH (cached for performance)"""
102
- if not hasattr(get_path_executables, '_cache'):
103
- executables = set()
104
- path_dirs = os.environ.get('PATH', '').split(os.pathsep)
105
- for path_dir in path_dirs:
106
- if os.path.isdir(path_dir):
107
- try:
108
- for item in os.listdir(path_dir):
109
- item_path = os.path.join(path_dir, item)
110
- if os.path.isfile(item_path) and os.access(item_path, os.X_OK):
111
- executables.add(item)
112
- except (PermissionError, OSError):
113
- continue
114
- get_path_executables._cache = sorted(list(executables))
115
- return get_path_executables._cache
116
-
117
-
118
- import logging
119
-
120
- # Set up completion logger
121
- completion_logger = logging.getLogger('npcsh.completion')
122
- completion_logger.setLevel(logging.WARNING) # Default to WARNING (quiet)
123
-
124
- # Add handler if not already present
125
- if not completion_logger.handlers:
126
- handler = logging.StreamHandler(sys.stderr)
127
- formatter = logging.Formatter('[%(name)s] %(message)s')
128
- handler.setFormatter(formatter)
129
- completion_logger.addHandler(handler)
130
-
131
- def make_completer(shell_state: ShellState):
132
- def complete(text: str, state_index: int) -> Optional[str]:
133
- """Main completion function"""
134
- try:
135
- buffer = readline.get_line_buffer()
136
- begidx = readline.get_begidx()
137
- endidx = readline.get_endidx()
138
-
139
- completion_logger.debug(f"text='{text}', buffer='{buffer}', begidx={begidx}, endidx={endidx}, state_index={state_index}")
140
-
141
- matches = []
142
-
143
- # Check if we're completing a slash command
144
- if begidx > 0 and buffer[begidx-1] == '/':
145
- completion_logger.debug(f"Slash command completion - text='{text}'")
146
- slash_commands = get_slash_commands(shell_state)
147
- completion_logger.debug(f"Available slash commands: {slash_commands}")
148
-
149
- if text == '':
150
- matches = [cmd[1:] for cmd in slash_commands]
151
- else:
152
- full_text = '/' + text
153
- matching_commands = [cmd for cmd in slash_commands if cmd.startswith(full_text)]
154
- matches = [cmd[1:] for cmd in matching_commands]
155
-
156
- completion_logger.debug(f"Slash command matches: {matches}")
157
-
158
- elif is_command_position(buffer, begidx):
159
- completion_logger.debug("Command position detected")
160
- bash_matches = [cmd for cmd in BASH_COMMANDS if cmd.startswith(text)]
161
- matches.extend(bash_matches)
162
-
163
- interactive_matches = [cmd for cmd in interactive_commands.keys() if cmd.startswith(text)]
164
- matches.extend(interactive_matches)
165
-
166
- if len(text) >= 1:
167
- path_executables = get_path_executables()
168
- exec_matches = [cmd for cmd in path_executables if cmd.startswith(text)]
169
- matches.extend(exec_matches[:20])
170
- else:
171
- completion_logger.debug("File completion")
172
- matches = get_file_completions(text)
173
-
174
- matches = sorted(list(set(matches)))
175
- completion_logger.debug(f"Final matches: {matches}")
176
-
177
- if state_index < len(matches):
178
- result = matches[state_index]
179
- completion_logger.debug(f"Returning: '{result}'")
180
- return result
181
- else:
182
- completion_logger.debug(f"No match for state_index {state_index}")
183
-
184
- except Exception as e:
185
- completion_logger.error(f"Exception in completion: {e}")
186
- completion_logger.debug("Exception details:", exc_info=True)
187
-
188
- return None
189
-
190
- return complete
191
-
192
- def get_slash_commands(state: ShellState) -> List[str]:
193
- """Get available slash commands from router and team"""
194
- commands = []
195
-
196
- completion_logger.debug("Getting slash commands...")
197
-
198
- # Router commands
199
- if router and hasattr(router, 'routes'):
200
- router_cmds = [f"/{cmd}" for cmd in router.routes.keys()]
201
- commands.extend(router_cmds)
202
- completion_logger.debug(f"Router commands: {router_cmds}")
203
-
204
- # Team jinxs
205
- if state.team and hasattr(state.team, 'jinxs_dict'):
206
- jinx_cmds = [f"/{jinx}" for jinx in state.team.jinxs_dict.keys()]
207
- commands.extend(jinx_cmds)
208
- completion_logger.debug(f"Jinx commands: {jinx_cmds}")
209
-
210
- # NPC names for switching
211
- if state.team and hasattr(state.team, 'npcs'):
212
- npc_cmds = [f"/{npc}" for npc in state.team.npcs.keys()]
213
- commands.extend(npc_cmds)
214
- completion_logger.debug(f"NPC commands: {npc_cmds}")
215
-
216
- # Mode switching commands
217
- mode_cmds = ['/cmd', '/agent', '/chat']
218
- commands.extend(mode_cmds)
219
- completion_logger.debug(f"Mode commands: {mode_cmds}")
220
-
221
- result = sorted(commands)
222
- completion_logger.debug(f"Final slash commands: {result}")
223
- return result
224
- def get_file_completions(text: str) -> List[str]:
225
- """Get file/directory completions"""
226
- try:
227
- if text.startswith('/'):
228
- basedir = os.path.dirname(text) or '/'
229
- prefix = os.path.basename(text)
230
- elif text.startswith('./') or text.startswith('../'):
231
- basedir = os.path.dirname(text) or '.'
232
- prefix = os.path.basename(text)
233
- else:
234
- basedir = '.'
235
- prefix = text
236
-
237
- if not os.path.exists(basedir):
238
- return []
239
-
240
- matches = []
241
- try:
242
- for item in os.listdir(basedir):
243
- if item.startswith(prefix):
244
- full_path = os.path.join(basedir, item)
245
- if basedir == '.':
246
- completion = item
247
- else:
248
- completion = os.path.join(basedir, item)
249
-
250
- # Just return the name, let readline handle spacing/slashes
251
- matches.append(completion)
252
- except (PermissionError, OSError):
253
- pass
254
-
255
- return sorted(matches)
256
- except Exception:
257
- return []
258
- def is_command_position(buffer: str, begidx: int) -> bool:
259
- """Determine if cursor is at a command position"""
260
- # Get the part of buffer before the current word
261
- before_word = buffer[:begidx]
262
-
263
- # Split by command separators
264
- parts = re.split(r'[|;&]', before_word)
265
- current_command_part = parts[-1].strip()
266
-
267
- # If there's nothing before the current word in this command part,
268
- # or only whitespace, we're at command position
269
- return len(current_command_part) == 0
270
-
271
-
272
- def readline_safe_prompt(prompt: str) -> str:
273
- ansi_escape = re.compile(r"(\033\[[0-9;]*[a-zA-Z])")
274
- return ansi_escape.sub(r"\001\1\002", prompt)
275
-
276
- def print_jinxs(jinxs):
277
- output = "Available jinxs:\n"
278
- for jinx in jinxs:
279
- output += f" {jinx.jinx_name}\n"
280
- output += f" Description: {jinx.description}\n"
281
- output += f" Inputs: {jinx.inputs}\n"
282
- return output
283
-
284
- def open_terminal_editor(command: str) -> str:
285
- try:
286
- os.system(command)
287
- return 'Terminal editor closed.'
288
- except Exception as e:
289
- return f"Error opening terminal editor: {e}"
290
-
291
- def get_multiline_input(prompt: str) -> str:
292
- lines = []
293
- current_prompt = prompt
294
- while True:
295
- try:
296
- line = input(current_prompt)
297
- if line.endswith("\\"):
298
- lines.append(line[:-1])
299
- current_prompt = readline_safe_prompt("> ")
300
- else:
301
- lines.append(line)
302
- break
303
- except EOFError:
304
- print("Goodbye!")
305
- sys.exit(0)
306
- return "\n".join(lines)
307
-
308
- def split_by_pipes(command: str) -> List[str]:
309
- parts = []
310
- current = ""
311
- in_single_quote = False
312
- in_double_quote = False
313
- escape = False
314
-
315
- for char in command:
316
- if escape:
317
- current += char
318
- escape = False
319
- elif char == '\\':
320
- escape = True
321
- current += char
322
- elif char == "'" and not in_double_quote:
323
- in_single_quote = not in_single_quote
324
- current += char
325
- elif char == '"' and not in_single_quote:
326
- in_double_quote = not in_single_quote
327
- current += char
328
- elif char == '|' and not in_single_quote and not in_double_quote:
329
- parts.append(current.strip())
330
- current = ""
331
- else:
332
- current += char
333
-
334
- if current:
335
- parts.append(current.strip())
336
- return parts
337
-
338
- def parse_command_safely(cmd: str) -> List[str]:
339
- try:
340
- return shlex.split(cmd)
341
- except ValueError as e:
342
- if "No closing quotation" in str(e):
343
- if cmd.count('"') % 2 == 1:
344
- cmd += '"'
345
- elif cmd.count("'") % 2 == 1:
346
- cmd += "'"
347
- try:
348
- return shlex.split(cmd)
349
- except ValueError:
350
- return cmd.split()
351
- else:
352
- return cmd.split()
353
-
354
- def get_file_color(filepath: str) -> tuple:
355
- if not os.path.exists(filepath):
356
- return "grey", []
357
- if os.path.isdir(filepath):
358
- return "blue", ["bold"]
359
- elif os.access(filepath, os.X_OK) and not os.path.isdir(filepath):
360
- return "green", ["bold"]
361
- elif filepath.endswith((".zip", ".tar", ".gz", ".bz2", ".xz", ".7z")):
362
- return "red", []
363
- elif filepath.endswith((".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff")):
364
- return "magenta", []
365
- elif filepath.endswith((".py", ".pyw")):
366
- return "yellow", []
367
- elif filepath.endswith((".sh", ".bash", ".zsh")):
368
- return "green", []
369
- elif filepath.endswith((".c", ".cpp", ".h", ".hpp")):
370
- return "cyan", []
371
- elif filepath.endswith((".js", ".ts", ".jsx", ".tsx")):
372
- return "yellow", []
373
- elif filepath.endswith((".html", ".css", ".scss", ".sass")):
374
- return "magenta", []
375
- elif filepath.endswith((".md", ".txt", ".log")):
376
- return "white", []
377
- elif os.path.basename(filepath).startswith("."):
378
- return "cyan", []
379
- else:
380
- return "white", []
381
-
382
- def format_file_listing(output: str) -> str:
383
- colored_lines = []
384
- current_dir = os.getcwd()
385
- for line in output.strip().split("\n"):
386
- parts = line.split()
387
- if not parts:
388
- colored_lines.append(line)
389
- continue
390
-
391
- filepath_guess = parts[-1]
392
- potential_path = os.path.join(current_dir, filepath_guess)
393
-
394
- color, attrs = get_file_color(potential_path)
395
- colored_filepath = colored(filepath_guess, color, attrs=attrs)
396
-
397
- if len(parts) > 1 :
398
- # Handle cases like 'ls -l' where filename is last
399
- colored_line = " ".join(parts[:-1] + [colored_filepath])
400
- else:
401
- # Handle cases where line is just the filename
402
- colored_line = colored_filepath
403
-
404
- colored_lines.append(colored_line)
405
-
406
- return "\n".join(colored_lines)
407
-
408
- def wrap_text(text: str, width: int = 80) -> str:
409
- lines = []
410
- for paragraph in text.split("\n"):
411
- if len(paragraph) > width:
412
- lines.extend(textwrap.wrap(paragraph, width=width, replace_whitespace=False, drop_whitespace=False))
413
- else:
414
- lines.append(paragraph)
415
- return "\n".join(lines)
416
-
417
- # --- Readline Setup and Completion ---
418
-
419
- def setup_readline() -> str:
420
- """Setup readline with history and completion"""
421
- try:
422
- readline.read_history_file(READLINE_HISTORY_FILE)
423
- readline.set_history_length(1000)
424
-
425
- # Don't set completer here - it will be set in run_repl with state
426
- readline.parse_and_bind("tab: complete")
427
-
428
- readline.parse_and_bind("set enable-bracketed-paste on")
429
- readline.parse_and_bind(r'"\C-r": reverse-search-history')
430
- readline.parse_and_bind(r'"\C-e": end-of-line')
431
- readline.parse_and_bind(r'"\C-a": beginning-of-line')
432
-
433
- return READLINE_HISTORY_FILE
434
-
435
- except FileNotFoundError:
436
- pass
437
- except OSError as e:
438
- print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
439
-
440
-
441
- def save_readline_history():
442
- try:
443
- readline.write_history_file(READLINE_HISTORY_FILE)
444
- except OSError as e:
445
- print(f"Warning: Could not write readline history file {READLINE_HISTORY_FILE}: {e}")
446
-
447
-
448
-
449
-
450
- valid_commands_list = list(router.routes.keys()) + list(interactive_commands.keys()) + ["cd", "exit", "quit"] + BASH_COMMANDS
451
-
452
-
453
-
454
-
455
- # --- Command Execution Logic ---
456
-
457
- def store_command_embeddings(command: str, output: Any, state: ShellState):
458
- if not chroma_client or not state.embedding_model or not state.embedding_provider:
459
- if not chroma_client: print("Warning: ChromaDB client not available for embeddings.", file=sys.stderr)
460
- return
461
- if not command and not output:
462
- return
463
-
464
- try:
465
- output_str = str(output) if output else ""
466
- if not command and not output_str: return # Avoid empty embeddings
467
-
468
- texts_to_embed = [command, output_str]
469
-
470
- embeddings = get_embeddings(
471
- texts_to_embed,
472
- state.embedding_model,
473
- state.embedding_provider,
474
- )
475
-
476
- if not embeddings or len(embeddings) != 2:
477
- print(f"Warning: Failed to generate embeddings for command: {command[:50]}...", file=sys.stderr)
478
- return
479
-
480
- timestamp = datetime.now().isoformat()
481
- npc_name = state.npc.name if isinstance(state.npc, NPC) else state.npc
482
-
483
- metadata = [
484
- {
485
- "type": "command", "timestamp": timestamp, "path": state.current_path,
486
- "npc": npc_name, "conversation_id": state.conversation_id,
487
- },
488
- {
489
- "type": "response", "timestamp": timestamp, "path": state.current_path,
490
- "npc": npc_name, "conversation_id": state.conversation_id,
491
- },
492
- ]
493
-
494
- collection_name = f"{state.embedding_provider}_{state.embedding_model}_embeddings"
495
- try:
496
- collection = chroma_client.get_or_create_collection(collection_name)
497
- ids = [f"cmd_{timestamp}_{hash(command)}", f"resp_{timestamp}_{hash(output_str)}"]
498
-
499
- collection.add(
500
- embeddings=embeddings,
501
- documents=texts_to_embed,
502
- metadatas=metadata,
503
- ids=ids,
504
- )
505
- except Exception as e:
506
- print(f"Warning: Failed to add embeddings to collection '{collection_name}': {e}", file=sys.stderr)
507
-
508
- except Exception as e:
509
- print(f"Warning: Failed to store embeddings: {e}", file=sys.stderr)
510
-
511
-
512
- def handle_interactive_command(cmd_parts: List[str], state: ShellState) -> Tuple[ShellState, str]:
513
- command_name = cmd_parts[0]
514
- print(f"Starting interactive {command_name} session...")
515
- try:
516
- return_code = start_interactive_session(
517
- interactive_commands[command_name], cmd_parts[1:]
518
- )
519
- output = f"Interactive {command_name} session ended with return code {return_code}"
520
- except Exception as e:
521
- output = f"Error starting interactive session {command_name}: {e}"
522
- return state, output
523
-
524
- def handle_cd_command(cmd_parts: List[str], state: ShellState) -> Tuple[ShellState, str]:
525
- original_path = os.getcwd()
526
- target_path = cmd_parts[1] if len(cmd_parts) > 1 else os.path.expanduser("~")
527
- try:
528
- os.chdir(target_path)
529
- state.current_path = os.getcwd()
530
- output = f"Changed directory to {state.current_path}"
531
- except FileNotFoundError:
532
- output = colored(f"cd: no such file or directory: {target_path}", "red")
533
- except Exception as e:
534
- output = colored(f"cd: error changing directory: {e}", "red")
535
- os.chdir(original_path) # Revert if error
536
-
537
- return state, output
538
-
539
-
540
- def handle_bash_command(
541
- cmd_parts: List[str],
542
- cmd_str: str,
543
- stdin_input: Optional[str],
544
- state: ShellState,
545
- ) -> Tuple[bool, str]:
546
- try:
547
- process = subprocess.Popen(
548
- cmd_parts,
549
- stdin=subprocess.PIPE if stdin_input is not None else None,
550
- stdout=subprocess.PIPE,
551
- stderr=subprocess.PIPE,
552
- text=True,
553
- cwd=state.current_path
554
- )
555
- stdout, stderr = process.communicate(input=stdin_input)
556
-
557
- if process.returncode != 0:
558
- return False, stderr.strip() if stderr else f"Command '{cmd_str}' failed with return code {process.returncode}."
559
-
560
- if stderr.strip():
561
- print(colored(f"stderr: {stderr.strip()}", "yellow"), file=sys.stderr)
562
-
563
- if cmd_parts[0] in ["ls", "find", "dir"]:
564
- return True, format_file_listing(stdout.strip())
565
-
566
- return True, stdout.strip()
567
-
568
- except FileNotFoundError:
569
- return False, f"Command not found: {cmd_parts[0]}"
570
- except PermissionError:
571
- return False, f"Permission denied: {cmd_str}"
572
-
573
- def _try_convert_type(value: str) -> Union[str, int, float, bool]:
574
- """Helper to convert string values to appropriate types."""
575
- if value.lower() in ['true', 'yes']:
576
- return True
577
- if value.lower() in ['false', 'no']:
578
- return False
579
- try:
580
- return int(value)
581
- except (ValueError, TypeError):
582
- pass
583
- try:
584
- return float(value)
585
- except (ValueError, TypeError):
586
- pass
587
- return value
588
-
589
- def parse_generic_command_flags(parts: List[str]) -> Tuple[Dict[str, Any], List[str]]:
590
- """
591
- Parses a list of command parts into a dictionary of keyword arguments and a list of positional arguments.
592
- Handles: -f val, --flag val, --flag=val, flag=val, --boolean-flag
593
- """
594
- parsed_kwargs = {}
595
- positional_args = []
596
- i = 0
597
- while i < len(parts):
598
- part = parts[i]
599
-
600
- if part.startswith('--'):
601
- key_part = part[2:]
602
- if '=' in key_part:
603
- key, value = key_part.split('=', 1)
604
- parsed_kwargs[key] = _try_convert_type(value)
605
- else:
606
- # Look ahead for a value
607
- if i + 1 < len(parts) and not parts[i + 1].startswith('-'):
608
- parsed_kwargs[key_part] = _try_convert_type(parts[i + 1])
609
- i += 1 # Consume the value
610
- else:
611
- parsed_kwargs[key_part] = True # Boolean flag
612
-
613
- elif part.startswith('-'):
614
- key = part[1:]
615
- # Look ahead for a value
616
- if i + 1 < len(parts) and not parts[i + 1].startswith('-'):
617
- parsed_kwargs[key] = _try_convert_type(parts[i + 1])
618
- i += 1 # Consume the value
619
- else:
620
- parsed_kwargs[key] = True # Boolean flag
621
-
622
- elif '=' in part and not part.startswith('-'):
623
- key, value = part.split('=', 1)
624
- parsed_kwargs[key] = _try_convert_type(value)
625
-
626
- else:
627
- positional_args.append(part)
628
-
629
- i += 1
630
-
631
- return parsed_kwargs, positional_args
632
-
633
-
634
- def should_skip_kg_processing(user_input: str, assistant_output: str) -> bool:
635
- """Determine if this interaction is too trivial for KG processing"""
636
-
637
- # Skip if user input is too short or trivial
638
- trivial_inputs = {
639
- '/sq', '/exit', '/quit', 'exit', 'quit', 'hey', 'hi', 'hello',
640
- 'fwah!', 'test', 'ping', 'ok', 'thanks', 'ty'
641
- }
642
-
643
- if user_input.lower().strip() in trivial_inputs:
644
- return True
645
-
646
- # Skip if user input is very short (less than 10 chars)
647
- if len(user_input.strip()) < 10:
648
- return True
649
-
650
- # Skip simple bash commands
651
- simple_bash = {'ls', 'pwd', 'cd', 'mkdir', 'touch', 'rm', 'mv', 'cp'}
652
- first_word = user_input.strip().split()[0] if user_input.strip() else ""
653
- if first_word in simple_bash:
654
- return True
655
-
656
- # Skip if assistant output is very short (less than 20 chars)
657
- if len(assistant_output.strip()) < 20:
658
- return True
659
-
660
- # Skip if it's just a mode exit message
661
- if "exiting" in assistant_output.lower() or "exited" in assistant_output.lower():
662
- return True
663
-
664
- return False
665
-
666
-
667
-
668
- def execute_slash_command(command: str, stdin_input: Optional[str], state: ShellState, stream: bool) -> Tuple[ShellState, Any]:
669
- """Executes slash commands using the router or checking NPC/Team jinxs."""
670
- all_command_parts = shlex.split(command)
671
- command_name = all_command_parts[0].lstrip('/')
672
- if command_name in ['n', 'npc']:
673
- npc_to_switch_to = all_command_parts[1] if len(all_command_parts) > 1 else None
674
- if npc_to_switch_to and state.team and npc_to_switch_to in state.team.npcs:
675
- state.npc = state.team.npcs[npc_to_switch_to]
676
- return state, f"Switched to NPC: {npc_to_switch_to}"
677
- else:
678
- available_npcs = list(state.team.npcs.keys()) if state.team else []
679
- return state, colored(f"NPC '{npc_to_switch_to}' not found. Available NPCs: {', '.join(available_npcs)}", "red")
680
- handler = router.get_route(command_name)
681
- if handler:
682
- parsed_flags, positional_args = parse_generic_command_flags(all_command_parts[1:])
683
-
684
- normalized_flags = normalize_and_expand_flags(parsed_flags)
685
-
686
- handler_kwargs = {
687
- 'stream': stream,
688
- 'team': state.team,
689
- 'messages': state.messages,
690
- 'api_url': state.api_url,
691
- 'api_key': state.api_key,
692
- 'stdin_input': stdin_input,
693
- 'positional_args': positional_args,
694
- 'plonk_context': state.team.shared_context.get('PLONK_CONTEXT') if state.team and hasattr(state.team, 'shared_context') else None,
695
-
696
- # Default chat model/provider
697
- 'model': state.npc.model if isinstance(state.npc, NPC) and state.npc.model else state.chat_model,
698
- 'provider': state.npc.provider if isinstance(state.npc, NPC) and state.npc.provider else state.chat_provider,
699
- 'npc': state.npc,
700
-
701
- # All other specific defaults
702
- 'sprovider': state.search_provider,
703
- 'emodel': state.embedding_model,
704
- 'eprovider': state.embedding_provider,
705
- 'igmodel': state.image_gen_model,
706
- 'igprovider': state.image_gen_provider,
707
- 'vgmodel': state.video_gen_model,
708
- 'vgprovider':state.video_gen_provider,
709
- 'vmodel': state.vision_model,
710
- 'vprovider': state.vision_provider,
711
- 'rmodel': state.reasoning_model,
712
- 'rprovider': state.reasoning_provider,
713
- }
714
-
715
- if len(normalized_flags)>0:
716
- kwarg_part = 'with kwargs: \n -' + '\n -'.join(f'{key}={item}' for key, item in normalized_flags.items())
717
- else:
718
- kwarg_part = ''
719
-
720
- # 4. Merge the clean, normalized flags. This will correctly overwrite defaults.
721
- render_markdown(f'- Calling {command_name} handler {kwarg_part} ')
722
- if 'model' in normalized_flags and 'provider' not in normalized_flags:
723
- # Call your existing, centralized lookup_provider function
724
- inferred_provider = lookup_provider(normalized_flags['model'])
725
- if inferred_provider:
726
- # Update the provider that will be used for this command.
727
- handler_kwargs['provider'] = inferred_provider
728
- print(colored(f"Info: Inferred provider '{inferred_provider}' for model '{normalized_flags['model']}'.", "cyan"))
729
- if 'provider' in normalized_flags and 'model' not in normalized_flags:
730
- # loop up mhandler_kwargs model's provider
731
- current_provider = lookup_provider(handler_kwargs['model'])
732
- if current_provider != normalized_flags['provider']:
733
- print(f'Please specify a model for the provider: {normalized_flags['provider']}')
734
- handler_kwargs.update(normalized_flags)
735
-
736
-
737
- try:
738
- result_dict = handler(command=command, **handler_kwargs)
739
- # add the output model and provider for the print_and_process_stream downstream processing
740
- if isinstance(result_dict, dict):
741
- state.messages = result_dict.get("messages", state.messages)
742
- return state, result_dict
743
- else:
744
- return state, result_dict
745
- except Exception as e:
746
- import traceback
747
- print(f"Error executing slash command '{command_name}':", file=sys.stderr)
748
- traceback.print_exc()
749
- return state, colored(f"Error executing slash command '{command_name}': {e}", "red")
750
- active_npc = state.npc if isinstance(state.npc, NPC) else None
751
- jinx_to_execute = None
752
- executor = None
753
- if active_npc and command_name in active_npc.jinxs_dict:
754
- jinx_to_execute = active_npc.jinxs_dict[command_name]
755
- executor = active_npc
756
- elif state.team and command_name in state.team.jinxs_dict:
757
- jinx_to_execute = state.team.jinxs_dict[command_name]
758
- executor = state.team
759
-
760
- if jinx_to_execute:
761
- args = command_parts[1:]
762
- try:
763
- jinx_output = jinx_to_execute.run(
764
- *args,
765
- state=state,
766
- stdin_input=stdin_input,
767
- messages=state.messages # Pass messages explicitly if needed
768
- )
769
- return state, jinx_output
770
- except Exception as e:
771
- import traceback
772
- print(f"Error executing jinx '{command_name}':", file=sys.stderr)
773
- traceback.print_exc()
774
- return state, colored(f"Error executing jinx '{command_name}': {e}", "red")
775
-
776
- if state.team and command_name in state.team.npcs:
777
- new_npc = state.team.npcs[command_name]
778
- state.npc = new_npc # Update state directly
779
- return state, f"Switched to NPC: {new_npc.name}"
780
-
781
- return state, colored(f"Unknown slash command or jinx: {command_name}", "red")
782
-
783
-
784
- def process_pipeline_command(
785
- cmd_segment: str,
786
- stdin_input: Optional[str],
787
- state: ShellState,
788
- stream_final: bool
789
- ) -> Tuple[ShellState, Any]:
790
-
791
- if not cmd_segment:
792
- return state, stdin_input
793
-
794
- available_models_all = get_locally_available_models(state.current_path)
795
- available_models_all_list = [item for key, item in available_models_all.items()]
796
-
797
- model_override, provider_override, cmd_cleaned = get_model_and_provider(
798
- cmd_segment, available_models_all_list
35
+ from npcsh._state import (
36
+ initial_state,
37
+ orange,
38
+ ShellState,
39
+ execute_command,
40
+ make_completer,
41
+ process_result,
42
+ readline_safe_prompt,
43
+ setup_shell,
44
+ get_multiline_input,
799
45
  )
800
- cmd_to_process = cmd_cleaned.strip()
801
- if not cmd_to_process:
802
- return state, stdin_input
803
-
804
- npc_model = state.npc.model if isinstance(state.npc, NPC) and state.npc.model else None
805
- npc_provider = state.npc.provider if isinstance(state.npc, NPC) and state.npc.provider else None
806
-
807
- exec_model = model_override or npc_model or state.chat_model
808
- exec_provider = provider_override or npc_provider or state.chat_provider
809
-
810
- if cmd_to_process.startswith("/"):
811
- return execute_slash_command(cmd_to_process, stdin_input, state, stream_final)
812
-
813
- cmd_parts = parse_command_safely(cmd_to_process)
814
- if not cmd_parts:
815
- return state, stdin_input
816
-
817
- command_name = cmd_parts[0]
818
-
819
- if command_name == "cd":
820
- return handle_cd_command(cmd_parts, state)
821
-
822
- if command_name in interactive_commands:
823
- return handle_interactive_command(cmd_parts, state)
824
-
825
- if validate_bash_command(cmd_parts):
826
- success, result = handle_bash_command(cmd_parts, cmd_to_process, stdin_input, state)
827
- if success:
828
- return state, result
829
- else:
830
- print(colored(f"Bash command failed: {result}. Asking LLM for a fix...", "yellow"), file=sys.stderr)
831
- fixer_prompt = f"The command '{cmd_to_process}' failed with the error: '{result}'. Provide the correct command."
832
- response = execute_llm_command(
833
- fixer_prompt,
834
- model=exec_model,
835
- provider=exec_provider,
836
- npc=state.npc,
837
- stream=stream_final,
838
- messages=state.messages
839
- )
840
- state.messages = response['messages']
841
- return state, response['response']
842
- else:
843
- full_llm_cmd = f"{cmd_to_process} {stdin_input}" if stdin_input else cmd_to_process
844
- path_cmd = 'The current working directory is: ' + state.current_path
845
- ls_files = 'Files in the current directory (full paths):\n' + "\n".join([os.path.join(state.current_path, f) for f in os.listdir(state.current_path)]) if os.path.exists(state.current_path) else 'No files found in the current directory.'
846
- platform_info = f"Platform: {platform.system()} {platform.release()} ({platform.machine()})"
847
- info = path_cmd + '\n' + ls_files + '\n' + platform_info + '\n'
848
-
849
- llm_result = check_llm_command(
850
- full_llm_cmd,
851
- model=exec_model,
852
- provider=exec_provider,
853
- api_url=state.api_url,
854
- api_key=state.api_key,
855
- npc=state.npc,
856
- team=state.team,
857
- messages=state.messages,
858
- images=state.attachments,
859
- stream=stream_final,
860
- context=info,
861
- )
862
- if isinstance(llm_result, dict):
863
- state.messages = llm_result.get("messages", state.messages)
864
- output = llm_result.get("output")
865
- return state, output
866
- else:
867
- return state, llm_result
868
- def check_mode_switch(command:str , state: ShellState):
869
- if command in ['/cmd', '/agent', '/chat',]:
870
- state.current_mode = command[1:]
871
- return True, state
872
-
873
- return False, state
874
- def execute_command(
875
- command: str,
876
- state: ShellState,
877
- ) -> Tuple[ShellState, Any]:
878
-
879
- if not command.strip():
880
- return state, ""
881
- mode_change, state = check_mode_switch(command, state)
882
- if mode_change:
883
- return state, 'Mode changed.'
884
-
885
- original_command_for_embedding = command
886
- commands = split_by_pipes(command)
887
- stdin_for_next = None
888
- final_output = None
889
- current_state = state
890
- npc_model = state.npc.model if isinstance(state.npc, NPC) and state.npc.model else None
891
- npc_provider = state.npc.provider if isinstance(state.npc, NPC) and state.npc.provider else None
892
- active_model = npc_model or state.chat_model
893
- active_provider = npc_provider or state.chat_provider
894
-
895
- if state.current_mode == 'agent':
896
- print(len(commands), commands)
897
- for i, cmd_segment in enumerate(commands):
898
-
899
- render_markdown(f'- executing command {i+1}/{len(commands)}')
900
- is_last_command = (i == len(commands) - 1)
901
46
 
902
- stream_this_segment = state.stream_output and not is_last_command
903
-
904
- try:
905
- current_state, output = process_pipeline_command(
906
- cmd_segment.strip(),
907
- stdin_for_next,
908
- current_state,
909
- stream_final=stream_this_segment
910
- )
911
-
912
- if is_last_command:
913
- return current_state, output
914
- if isinstance(output, str):
915
- stdin_for_next = output
916
- elif not isinstance(output, str):
917
- try:
918
- if stream_this_segment:
919
- full_stream_output = print_and_process_stream_with_markdown(output,
920
- state.npc.model,
921
- state.npc.provider)
922
- stdin_for_next = full_stream_output
923
- if is_last_command:
924
- final_output = full_stream_output
925
- except:
926
- if output is not None: # Try converting other types to string
927
- try:
928
- stdin_for_next = str(output)
929
- except Exception:
930
- print(f"Warning: Cannot convert output to string for piping: {type(output)}", file=sys.stderr)
931
- stdin_for_next = None
932
- else: # Output was None
933
- stdin_for_next = None
934
-
935
-
936
- except Exception as pipeline_error:
937
- import traceback
938
- traceback.print_exc()
939
- error_msg = colored(f"Error in pipeline stage {i+1} ('{cmd_segment[:50]}...'): {pipeline_error}", "red")
940
- # Return the state as it was when the error occurred, and the error message
941
- return current_state, error_msg
942
-
943
- # Store embeddings using the final state
944
- if final_output is not None and isinstance(final_output,str):
945
- store_command_embeddings(original_command_for_embedding, final_output, current_state)
946
-
947
- # Return the final state and the final output
948
- return current_state, final_output
949
-
950
-
951
- elif state.current_mode == 'chat':
952
- # Only treat as bash if it looks like a shell command (starts with known command or is a slash command)
953
- cmd_parts = parse_command_safely(command)
954
- is_probably_bash = (
955
- cmd_parts
956
- and (
957
- cmd_parts[0] in interactive_commands
958
- or cmd_parts[0] in BASH_COMMANDS
959
- or command.strip().startswith("./")
960
- or command.strip().startswith("/")
961
- )
962
- )
963
- if is_probably_bash:
964
- try:
965
- command_name = cmd_parts[0]
966
- if command_name in interactive_commands:
967
- return handle_interactive_command(cmd_parts, state)
968
- elif command_name == "cd":
969
- return handle_cd_command(cmd_parts, state)
970
- else:
971
- try:
972
- bash_state, bash_output = handle_bash_command(cmd_parts, command, None, state)
973
- return bash_state, bash_output
974
- except Exception as bash_err:
975
- return state, colored(f"Bash execution failed: {bash_err}", "red")
976
- except Exception:
977
- pass # Fall through to LLM
978
-
979
- # Otherwise, treat as chat (LLM)
980
- response = get_llm_response(
981
- command,
982
- model=active_model,
983
- provider=active_provider,
984
- npc=state.npc,
985
- stream=state.stream_output,
986
- messages=state.messages
987
- )
988
- state.messages = response['messages']
989
- return state, response['response']
990
-
991
- elif state.current_mode == 'cmd':
992
-
993
- response = execute_llm_command(command,
994
- model=active_model,
995
- provider=active_provider,
996
- npc = state.npc,
997
- stream = state.stream_output,
998
- messages = state.messages)
999
- state.messages = response['messages']
1000
- return state, response['response']
1001
-
1002
- """
1003
- # to be replaced with a standalone corca mode
1004
-
1005
- elif state.current_mode == 'ride':
1006
- # Allow bash commands in /ride mode
1007
- cmd_parts = parse_command_safely(command)
1008
- is_probably_bash = (
1009
- cmd_parts
1010
- and (
1011
- cmd_parts[0] in interactive_commands
1012
- or cmd_parts[0] in BASH_COMMANDS
1013
- or command.strip().startswith("./")
1014
- or command.strip().startswith("/")
1015
- )
1016
- )
1017
- if is_probably_bash:
1018
- try:
1019
- command_name = cmd_parts[0]
1020
- if command_name in interactive_commands:
1021
- return handle_interactive_command(cmd_parts, state)
1022
- elif command_name == "cd":
1023
- return handle_cd_command(cmd_parts, state)
1024
- else:
1025
- try:
1026
- bash_state, bash_output = handle_bash_command(cmd_parts, command, None, state)
1027
- return bash_state, bash_output
1028
- except Exception as bash_err:
1029
- return state, colored(f"Bash execution failed: {bash_err}", "red")
1030
- except Exception:
1031
- return state, colored("Failed to parse or execute bash command.", "red")
1032
-
1033
- # Otherwise, run the agentic ride loop
1034
- return agentic_ride_loop(command, state)
1035
- """
1036
-
1037
-
1038
- def check_deprecation_warnings():
1039
- if os.getenv("NPCSH_MODEL"):
1040
- cprint(
1041
- "Deprecation Warning: NPCSH_MODEL/PROVIDER deprecated. Use NPCSH_CHAT_MODEL/PROVIDER.",
1042
- "yellow",
1043
- )
1044
47
 
1045
48
  def print_welcome_message():
49
+ '''
50
+ function for printing npcsh graphic
51
+ '''
52
+
1046
53
  print(
1047
54
  """
1048
55
  Welcome to \033[1;94mnpc\033[0m\033[1;38;5;202msh\033[0m!
@@ -1060,296 +67,11 @@ Begin by asking a question, issuing a bash command, or typing '/help' for more i
1060
67
  """
1061
68
  )
1062
69
 
1063
- def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
1064
- check_deprecation_warnings()
1065
- setup_npcsh_config()
1066
-
1067
- db_path = os.getenv("NPCSH_DB_PATH", HISTORY_DB_DEFAULT_PATH)
1068
- db_path = os.path.expanduser(db_path)
1069
- os.makedirs(os.path.dirname(db_path), exist_ok=True)
1070
- command_history = CommandHistory(db_path)
1071
-
1072
-
1073
- if not is_npcsh_initialized():
1074
- print("Initializing NPCSH...")
1075
- initialize_base_npcs_if_needed(db_path)
1076
- print("NPCSH initialization complete. Restart or source ~/.npcshrc.")
1077
-
1078
-
1079
-
1080
- try:
1081
- history_file = setup_readline()
1082
- atexit.register(save_readline_history)
1083
- atexit.register(command_history.close)
1084
- except:
1085
- pass
1086
-
1087
- project_team_path = os.path.abspath(PROJECT_NPC_TEAM_PATH)
1088
- global_team_path = os.path.expanduser(DEFAULT_NPC_TEAM_PATH)
1089
- team_dir = None
1090
- default_forenpc_name = None
1091
-
1092
- if os.path.exists(project_team_path):
1093
- team_dir = project_team_path
1094
- default_forenpc_name = "forenpc"
1095
- else:
1096
- if not os.path.exists('.npcsh_global'):
1097
- resp = input(f"No npc_team found in {os.getcwd()}. Create a new team here? [Y/n]: ").strip().lower()
1098
- if resp in ("", "y", "yes"):
1099
- team_dir = project_team_path
1100
- os.makedirs(team_dir, exist_ok=True)
1101
- default_forenpc_name = "forenpc"
1102
- forenpc_directive = input(
1103
- f"Enter a primary directive for {default_forenpc_name} (default: 'You are the forenpc of the team...'): "
1104
- ).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."
1105
- forenpc_model = input("Enter a model for your forenpc (default: llama3.2): ").strip() or "llama3.2"
1106
- forenpc_provider = input("Enter a provider for your forenpc (default: ollama): ").strip() or "ollama"
1107
-
1108
- with open(os.path.join(team_dir, f"{default_forenpc_name}.npc"), "w") as f:
1109
- yaml.dump({
1110
- "name": default_forenpc_name, "primary_directive": forenpc_directive,
1111
- "model": forenpc_model, "provider": forenpc_provider
1112
- }, f)
1113
-
1114
- ctx_path = os.path.join(team_dir, "team.ctx")
1115
- folder_context = input("Enter a short description for this project/team (optional): ").strip()
1116
- team_ctx_data = {
1117
- "forenpc": default_forenpc_name, "model": forenpc_model,
1118
- "provider": forenpc_provider, "api_key": None, "api_url": None,
1119
- "context": folder_context if folder_context else None
1120
- }
1121
- use_jinxs = input("Use global jinxs folder (g) or copy to this project (c)? [g/c, default: g]: ").strip().lower()
1122
- if use_jinxs == "c":
1123
- global_jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
1124
- if os.path.exists(global_jinxs_dir):
1125
- shutil.copytree(global_jinxs_dir, team_dir, dirs_exist_ok=True)
1126
- else:
1127
- team_ctx_data["use_global_jinxs"] = True
1128
-
1129
- with open(ctx_path, "w") as f:
1130
- yaml.dump(team_ctx_data, f)
1131
- else:
1132
- render_markdown('From now on, npcsh will assume you will use the global team when activating from this folder. \n If you change your mind and want to initialize a team, use /init from within npcsh, `npc init` or `rm .npcsh_global` from the current working directory.')
1133
- with open(".npcsh_global", "w") as f:
1134
- pass
1135
- team_dir = global_team_path
1136
- default_forenpc_name = "sibiji"
1137
- elif os.path.exists(global_team_path):
1138
- team_dir = global_team_path
1139
- default_forenpc_name = "sibiji"
1140
-
1141
-
1142
- team_ctx = {}
1143
- for filename in os.listdir(team_dir):
1144
- if filename.endswith(".ctx"):
1145
- try:
1146
- with open(os.path.join(team_dir, filename), "r") as f:
1147
- team_ctx = yaml.safe_load(f) or {}
1148
- break
1149
- except Exception as e:
1150
- print(f"Warning: Could not load context file {filename}: {e}")
1151
-
1152
- forenpc_name = team_ctx.get("forenpc", default_forenpc_name)
1153
- #render_markdown(f"- Using forenpc: {forenpc_name}")
1154
-
1155
- if team_ctx.get("use_global_jinxs", False):
1156
- jinxs_dir = os.path.expanduser("~/.npcsh/npc_team/jinxs")
1157
- else:
1158
- jinxs_dir = os.path.join(team_dir, "jinxs")
1159
-
1160
- jinxs_list = load_jinxs_from_directory(jinxs_dir)
1161
- jinxs_dict = {jinx.jinx_name: jinx for jinx in jinxs_list}
1162
-
1163
- forenpc_obj = None
1164
- forenpc_path = os.path.join(team_dir, f"{forenpc_name}.npc")
1165
-
1166
-
1167
- #render_markdown('- Loaded team context'+ json.dumps(team_ctx, indent=2))
1168
-
1169
-
1170
-
1171
- if os.path.exists(forenpc_path):
1172
- forenpc_obj = NPC(file = forenpc_path,
1173
- jinxs=jinxs_list)
1174
- if forenpc_obj.model is None:
1175
- forenpc_obj.model= team_ctx.get("model", initial_state.chat_model)
1176
- if forenpc_obj.provider is None:
1177
- forenpc_obj.provider=team_ctx.get('provider', initial_state.chat_provider)
1178
-
1179
- else:
1180
- print(f"Warning: Forenpc file '{forenpc_name}.npc' not found in {team_dir}.")
1181
-
1182
- team = Team(team_path=team_dir,
1183
- forenpc=forenpc_obj,
1184
- jinxs=jinxs_dict)
1185
-
1186
- for npc_name, npc_obj in team.npcs.items():
1187
- if not npc_obj.model:
1188
- npc_obj.model = initial_state.chat_model
1189
- if not npc_obj.provider:
1190
- npc_obj.provider = initial_state.chat_provider
1191
-
1192
- # Also apply to the forenpc specifically
1193
- if team.forenpc and isinstance(team.forenpc, NPC):
1194
- if not team.forenpc.model:
1195
- team.forenpc.model = initial_state.chat_model
1196
- if not team.forenpc.provider:
1197
- team.forenpc.provider = initial_state.chat_provider
1198
- team_name_from_ctx = team_ctx.get("name")
1199
- if team_name_from_ctx:
1200
- team.name = team_name_from_ctx
1201
- elif team_dir and os.path.basename(team_dir) != 'npc_team':
1202
- team.name = os.path.basename(team_dir)
1203
- else:
1204
- team.name = "global_team" # fallback for ~/.npcsh/npc_team
1205
-
1206
- return command_history, team, forenpc_obj
1207
-
1208
- # In your main npcsh.py file
1209
-
1210
- def process_result(
1211
- user_input: str,
1212
- result_state: ShellState,
1213
- output: Any,
1214
- command_history: CommandHistory
1215
- ):
1216
- # --- Part 1: Save Conversation & Determine Output ---
1217
-
1218
- # Define team and NPC names early for consistent logging
1219
- team_name = result_state.team.name if result_state.team else "__none__"
1220
- npc_name = result_state.npc.name if isinstance(result_state.npc, NPC) else "__none__"
1221
-
1222
- # Determine the actual NPC object to use for this turn's operations
1223
- active_npc = result_state.npc if isinstance(result_state.npc, NPC) else NPC(
1224
- name="default",
1225
- model=result_state.chat_model,
1226
- provider=result_state.chat_provider
1227
- )
1228
-
1229
- save_conversation_message(
1230
- command_history,
1231
- result_state.conversation_id,
1232
- "user",
1233
- user_input,
1234
- wd=result_state.current_path,
1235
- model=active_npc.model,
1236
- provider=active_npc.provider,
1237
- npc=npc_name,
1238
- team=team_name,
1239
- attachments=result_state.attachments,
1240
- )
1241
- result_state.attachments = None
1242
-
1243
- final_output_str = None
1244
- output_content = output.get('output') if isinstance(output, dict) else output
1245
- model_for_stream = output.get('model', active_npc.model) if isinstance(output, dict) else active_npc.model
1246
- provider_for_stream = output.get('provider', active_npc.provider) if isinstance(output, dict) else active_npc.provider
1247
-
1248
- print('\n')
1249
- if user_input =='/help':
1250
- render_markdown(output.get('output'))
1251
- elif result_state.stream_output:
1252
-
1253
-
1254
- final_output_str = print_and_process_stream_with_markdown(output_content, model_for_stream, provider_for_stream)
1255
- elif output_content is not None:
1256
- final_output_str = str(output_content)
1257
- render_markdown(final_output_str)
1258
-
1259
- if final_output_str:
1260
-
1261
- if result_state.messages and (not result_state.messages or result_state.messages[-1].get("role") != "assistant"):
1262
- result_state.messages.append({"role": "assistant", "content": final_output_str})
1263
- save_conversation_message(
1264
- command_history,
1265
- result_state.conversation_id,
1266
- "assistant",
1267
- final_output_str,
1268
- wd=result_state.current_path,
1269
- model=active_npc.model,
1270
- provider=active_npc.provider,
1271
- npc=npc_name,
1272
- team=team_name,
1273
- )
1274
-
1275
- conversation_turn_text = f"User: {user_input}\nAssistant: {final_output_str}"
1276
- engine = command_history.engine
1277
-
1278
-
1279
- if result_state.build_kg:
1280
- try:
1281
- if not should_skip_kg_processing(user_input, final_output_str):
1282
-
1283
- npc_kg = load_kg_from_db(engine, team_name, npc_name, result_state.current_path)
1284
- evolved_npc_kg, _ = kg_evolve_incremental(
1285
- existing_kg=npc_kg,
1286
- new_content_text=conversation_turn_text,
1287
- model=active_npc.model,
1288
- provider=active_npc.provider,
1289
- get_concepts=True,
1290
- link_concepts_facts = False,
1291
- link_concepts_concepts = False,
1292
- link_facts_facts = False,
1293
-
1294
-
1295
- )
1296
- save_kg_to_db(engine,
1297
- evolved_npc_kg,
1298
- team_name,
1299
- npc_name,
1300
- result_state.current_path)
1301
- except Exception as e:
1302
- print(colored(f"Error during real-time KG evolution: {e}", "red"))
1303
-
1304
- # --- Part 3: Periodic Team Context Suggestions ---
1305
- result_state.turn_count += 1
1306
-
1307
- if result_state.turn_count > 0 and result_state.turn_count % 10 == 0:
1308
- print(colored("\nChecking for potential team improvements...", "cyan"))
1309
- try:
1310
- summary = breathe(messages=result_state.messages[-20:],
1311
- npc=active_npc)
1312
- characterization = summary.get('output')
1313
-
1314
- if characterization and result_state.team:
1315
- team_ctx_path = os.path.join(result_state.team.team_path, "team.ctx")
1316
- ctx_data = {}
1317
- if os.path.exists(team_ctx_path):
1318
- with open(team_ctx_path, 'r') as f:
1319
- ctx_data = yaml.safe_load(f) or {}
1320
- current_context = ctx_data.get('context', '')
1321
-
1322
- prompt = f"""Based on this characterization: {characterization},
1323
-
1324
- suggest changes (additions, deletions, edits) to the team's context.
1325
- Additions need not be fully formed sentences and can simply be equations, relationships, or other plain clear items.
1326
-
1327
- Current Context: "{current_context}".
1328
-
1329
- Respond with JSON: {{"suggestion": "Your sentence."
1330
- }}"""
1331
- response = get_llm_response(prompt, npc=active_npc, format="json")
1332
- suggestion = response.get("response", {}).get("suggestion")
1333
-
1334
- if suggestion:
1335
- new_context = (current_context + " " + suggestion).strip()
1336
- print(colored(f"{result_state.npc.name} suggests updating team context:", "yellow"))
1337
- print(f" - OLD: {current_context}\n + NEW: {new_context}")
1338
- if input("Apply? [y/N]: ").strip().lower() == 'y':
1339
- ctx_data['context'] = new_context
1340
- with open(team_ctx_path, 'w') as f:
1341
- yaml.dump(ctx_data, f)
1342
- print(colored("Team context updated.", "green"))
1343
- else:
1344
- print("Suggestion declined.")
1345
- except Exception as e:
1346
- import traceback
1347
- print(colored(f"Could not generate team suggestions: {e}", "yellow"))
1348
- traceback.print_exc()
1349
-
1350
-
1351
70
 
1352
71
  def run_repl(command_history: CommandHistory, initial_state: ShellState):
72
+ '''
73
+ Func for running the npcsh repl
74
+ '''
1353
75
  state = initial_state
1354
76
  print_welcome_message()
1355
77
 
@@ -1377,14 +99,13 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1377
99
  print(colored("Processing and archiving all session knowledge...", "cyan"))
1378
100
 
1379
101
  engine = command_history.engine
1380
- integrator_npc = NPC(name="integrator", model=current_state.chat_model, provider=current_state.chat_provider)
1381
102
 
1382
- # Process each unique scope that was active during the session
103
+
1383
104
  for team_name, npc_name, path in session_scopes:
1384
105
  try:
1385
106
  print(f" -> Archiving knowledge for: T='{team_name}', N='{npc_name}', P='{path}'")
1386
107
 
1387
- # Get all messages for the current conversation that happened in this specific path
108
+
1388
109
  convo_id = current_state.conversation_id
1389
110
  all_messages = command_history.get_conversations_by_id(convo_id)
1390
111
 
@@ -1399,15 +120,16 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1399
120
  print(" ...No content for this scope, skipping.")
1400
121
  continue
1401
122
 
1402
- # Load the existing KG for this specific, real scope
123
+
1403
124
  current_kg = load_kg_from_db(engine, team_name, npc_name, path)
1404
125
 
1405
- # Evolve it with the full text from the session for this scope
126
+
1406
127
  evolved_kg, _ = kg_evolve_incremental(
1407
128
  existing_kg=current_kg,
1408
129
  new_content_text=full_text,
1409
- model=integrator_npc.model,
1410
- provider=integrator_npc.provider,
130
+ model=current_state.npc.model,
131
+ provider=current_state.npc.provider,
132
+ npc= current_state.npc,
1411
133
  get_concepts=True,
1412
134
  link_concepts_facts = True,
1413
135
  link_concepts_concepts = True,
@@ -1416,7 +138,11 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1416
138
  )
1417
139
 
1418
140
  # Save the updated KG back to the database under the same exact scope
1419
- save_kg_to_db(engine, evolved_kg, team_name, npc_name, path)
141
+ save_kg_to_db(engine,
142
+ evolved_kg,
143
+ team_name,
144
+ npc_name,
145
+ path)
1420
146
 
1421
147
  except Exception as e:
1422
148
  import traceback
@@ -1473,21 +199,22 @@ def run_repl(command_history: CommandHistory, initial_state: ShellState):
1473
199
  npc_name = state.npc.name if isinstance(state.npc, NPC) else "__none__"
1474
200
  session_scopes.add((team_name, npc_name, state.current_path))
1475
201
 
1476
- state, output = execute_command(user_input, state)
1477
- process_result(user_input, state,
202
+ state, output = execute_command(user_input,
203
+ state,
204
+ review = True,
205
+ router=router)
206
+ process_result(user_input,
207
+ state,
1478
208
  output,
1479
209
  command_history)
1480
210
 
1481
211
  except KeyboardInterrupt:
1482
212
  if is_windows:
1483
- # On Windows, Ctrl+C cancels the current input line, show prompt again
1484
213
  print("^C")
1485
214
  continue
1486
215
  else:
1487
- # On Unix, Ctrl+C exits the shell as before
1488
216
  exit_shell(state)
1489
217
  except EOFError:
1490
- # Ctrl+D: exit shell cleanly
1491
218
  exit_shell(state)
1492
219
  def main() -> None:
1493
220
  parser = argparse.ArgumentParser(description="npcsh - An NPC-powered shell.")
@@ -1502,11 +229,7 @@ def main() -> None:
1502
229
  command_history, team, default_npc = setup_shell()
1503
230
 
1504
231
  initial_state.npc = default_npc
1505
- initial_state.team = team
1506
-
1507
-
1508
- # add a -g global command to indicate if to use the global or project, otherwise go thru normal flow
1509
-
232
+ initial_state.team = team
1510
233
  if args.command:
1511
234
  state = initial_state
1512
235
  state.current_path = os.getcwd()