npcsh 0.3.31__py3-none-any.whl → 1.0.0__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.
Files changed (91) hide show
  1. npcsh/_state.py +942 -0
  2. npcsh/alicanto.py +1074 -0
  3. npcsh/guac.py +785 -0
  4. npcsh/mcp_helpers.py +357 -0
  5. npcsh/mcp_npcsh.py +822 -0
  6. npcsh/mcp_server.py +184 -0
  7. npcsh/npc.py +218 -0
  8. npcsh/npcsh.py +1161 -0
  9. npcsh/plonk.py +387 -269
  10. npcsh/pti.py +234 -0
  11. npcsh/routes.py +958 -0
  12. npcsh/spool.py +315 -0
  13. npcsh/wander.py +550 -0
  14. npcsh/yap.py +573 -0
  15. npcsh-1.0.0.dist-info/METADATA +596 -0
  16. npcsh-1.0.0.dist-info/RECORD +21 -0
  17. {npcsh-0.3.31.dist-info → npcsh-1.0.0.dist-info}/WHEEL +1 -1
  18. npcsh-1.0.0.dist-info/entry_points.txt +9 -0
  19. {npcsh-0.3.31.dist-info → npcsh-1.0.0.dist-info}/licenses/LICENSE +1 -1
  20. npcsh/audio.py +0 -210
  21. npcsh/cli.py +0 -545
  22. npcsh/command_history.py +0 -566
  23. npcsh/conversation.py +0 -291
  24. npcsh/data_models.py +0 -46
  25. npcsh/dataframes.py +0 -163
  26. npcsh/embeddings.py +0 -168
  27. npcsh/helpers.py +0 -641
  28. npcsh/image.py +0 -298
  29. npcsh/image_gen.py +0 -79
  30. npcsh/knowledge_graph.py +0 -1006
  31. npcsh/llm_funcs.py +0 -2027
  32. npcsh/load_data.py +0 -83
  33. npcsh/main.py +0 -5
  34. npcsh/model_runner.py +0 -189
  35. npcsh/npc_compiler.py +0 -2870
  36. npcsh/npc_sysenv.py +0 -383
  37. npcsh/npc_team/assembly_lines/test_pipeline.py +0 -181
  38. npcsh/npc_team/corca.npc +0 -13
  39. npcsh/npc_team/foreman.npc +0 -7
  40. npcsh/npc_team/npcsh.ctx +0 -11
  41. npcsh/npc_team/sibiji.npc +0 -4
  42. npcsh/npc_team/templates/analytics/celona.npc +0 -0
  43. npcsh/npc_team/templates/hr_support/raone.npc +0 -0
  44. npcsh/npc_team/templates/humanities/eriane.npc +0 -4
  45. npcsh/npc_team/templates/it_support/lineru.npc +0 -0
  46. npcsh/npc_team/templates/marketing/slean.npc +0 -4
  47. npcsh/npc_team/templates/philosophy/maurawa.npc +0 -0
  48. npcsh/npc_team/templates/sales/turnic.npc +0 -4
  49. npcsh/npc_team/templates/software/welxor.npc +0 -0
  50. npcsh/npc_team/tools/bash_executer.tool +0 -32
  51. npcsh/npc_team/tools/calculator.tool +0 -8
  52. npcsh/npc_team/tools/code_executor.tool +0 -16
  53. npcsh/npc_team/tools/generic_search.tool +0 -27
  54. npcsh/npc_team/tools/image_generation.tool +0 -25
  55. npcsh/npc_team/tools/local_search.tool +0 -149
  56. npcsh/npc_team/tools/npcsh_executor.tool +0 -9
  57. npcsh/npc_team/tools/screen_cap.tool +0 -27
  58. npcsh/npc_team/tools/sql_executor.tool +0 -26
  59. npcsh/response.py +0 -623
  60. npcsh/search.py +0 -248
  61. npcsh/serve.py +0 -1460
  62. npcsh/shell.py +0 -538
  63. npcsh/shell_helpers.py +0 -3529
  64. npcsh/stream.py +0 -700
  65. npcsh/video.py +0 -49
  66. npcsh-0.3.31.data/data/npcsh/npc_team/bash_executer.tool +0 -32
  67. npcsh-0.3.31.data/data/npcsh/npc_team/calculator.tool +0 -8
  68. npcsh-0.3.31.data/data/npcsh/npc_team/celona.npc +0 -0
  69. npcsh-0.3.31.data/data/npcsh/npc_team/code_executor.tool +0 -16
  70. npcsh-0.3.31.data/data/npcsh/npc_team/corca.npc +0 -13
  71. npcsh-0.3.31.data/data/npcsh/npc_team/eriane.npc +0 -4
  72. npcsh-0.3.31.data/data/npcsh/npc_team/foreman.npc +0 -7
  73. npcsh-0.3.31.data/data/npcsh/npc_team/generic_search.tool +0 -27
  74. npcsh-0.3.31.data/data/npcsh/npc_team/image_generation.tool +0 -25
  75. npcsh-0.3.31.data/data/npcsh/npc_team/lineru.npc +0 -0
  76. npcsh-0.3.31.data/data/npcsh/npc_team/local_search.tool +0 -149
  77. npcsh-0.3.31.data/data/npcsh/npc_team/maurawa.npc +0 -0
  78. npcsh-0.3.31.data/data/npcsh/npc_team/npcsh.ctx +0 -11
  79. npcsh-0.3.31.data/data/npcsh/npc_team/npcsh_executor.tool +0 -9
  80. npcsh-0.3.31.data/data/npcsh/npc_team/raone.npc +0 -0
  81. npcsh-0.3.31.data/data/npcsh/npc_team/screen_cap.tool +0 -27
  82. npcsh-0.3.31.data/data/npcsh/npc_team/sibiji.npc +0 -4
  83. npcsh-0.3.31.data/data/npcsh/npc_team/slean.npc +0 -4
  84. npcsh-0.3.31.data/data/npcsh/npc_team/sql_executor.tool +0 -26
  85. npcsh-0.3.31.data/data/npcsh/npc_team/test_pipeline.py +0 -181
  86. npcsh-0.3.31.data/data/npcsh/npc_team/turnic.npc +0 -4
  87. npcsh-0.3.31.data/data/npcsh/npc_team/welxor.npc +0 -0
  88. npcsh-0.3.31.dist-info/METADATA +0 -1853
  89. npcsh-0.3.31.dist-info/RECORD +0 -76
  90. npcsh-0.3.31.dist-info/entry_points.txt +0 -3
  91. {npcsh-0.3.31.dist-info → npcsh-1.0.0.dist-info}/top_level.txt +0 -0
npcsh/_state.py ADDED
@@ -0,0 +1,942 @@
1
+
2
+ from colorama import Fore, Back, Style
3
+
4
+
5
+ from datetime import datetime
6
+ from dotenv import load_dotenv
7
+
8
+ import re
9
+ import os
10
+ from termcolor import colored
11
+
12
+
13
+
14
+ from typing import Dict, List
15
+ import re
16
+ import sqlite3
17
+ from datetime import datetime
18
+ import logging
19
+ import textwrap
20
+ from termcolor import colored
21
+ import sys
22
+ import platform
23
+
24
+
25
+ def get_npc_path(npc_name: str, db_path: str) -> str:
26
+ # First, check in project npc_team directory
27
+ project_npc_team_dir = os.path.abspath("./npc_team")
28
+ project_npc_path = os.path.join(project_npc_team_dir, f"{npc_name}.npc")
29
+
30
+ # Then, check in global npc_team directory
31
+ user_npc_team_dir = os.path.expanduser("~/.npcsh/npc_team")
32
+ global_npc_path = os.path.join(user_npc_team_dir, f"{npc_name}.npc")
33
+
34
+ # Check database for compiled NPCs
35
+ try:
36
+ with sqlite3.connect(db_path) as conn:
37
+ cursor = conn.cursor()
38
+ query = f"SELECT source_path FROM compiled_npcs WHERE name = '{npc_name}'"
39
+ cursor.execute(query)
40
+ result = cursor.fetchone()
41
+ if result:
42
+ return result[0]
43
+
44
+ except Exception as e:
45
+ try:
46
+ with sqlite3.connect(db_path) as conn:
47
+ cursor = conn.cursor()
48
+ query = f"SELECT source_path FROM compiled_npcs WHERE name = {npc_name}"
49
+ cursor.execute(query)
50
+ result = cursor.fetchone()
51
+ if result:
52
+ return result[0]
53
+ except Exception as e:
54
+ print(f"Database query error: {e}")
55
+
56
+ # Fallback to file paths
57
+ if os.path.exists(project_npc_path):
58
+ return project_npc_path
59
+
60
+ if os.path.exists(global_npc_path):
61
+ return global_npc_path
62
+
63
+ raise ValueError(f"NPC file not found: {npc_name}")
64
+
65
+
66
+ def initialize_base_npcs_if_needed(db_path: str) -> None:
67
+ """
68
+ Function Description:
69
+ This function initializes the base NPCs if they are not already in the database.
70
+ Args:
71
+ db_path: The path to the database file.
72
+ Keyword Args:
73
+
74
+ None
75
+ Returns:
76
+ None
77
+ """
78
+
79
+ if is_npcsh_initialized():
80
+ return
81
+
82
+ conn = sqlite3.connect(db_path)
83
+ cursor = conn.cursor()
84
+
85
+ # Create the compiled_npcs table if it doesn't exist
86
+ cursor.execute(
87
+ """
88
+ CREATE TABLE IF NOT EXISTS compiled_npcs (
89
+ name TEXT PRIMARY KEY,
90
+ source_path TEXT NOT NULL,
91
+ compiled_content TEXT
92
+ )
93
+ """
94
+ )
95
+
96
+ # Get the path to the npc_team directory in the package
97
+ package_dir = os.path.dirname(__file__)
98
+ package_npc_team_dir = os.path.join(package_dir, "npc_team")
99
+
100
+
101
+
102
+ # User's global npc_team directory
103
+ user_npc_team_dir = os.path.expanduser("~/.npcsh/npc_team")
104
+
105
+ user_jinxs_dir = os.path.join(user_npc_team_dir, "jinxs")
106
+ user_templates_dir = os.path.join(user_npc_team_dir, "templates")
107
+ os.makedirs(user_npc_team_dir, exist_ok=True)
108
+ os.makedirs(user_jinxs_dir, exist_ok=True)
109
+ os.makedirs(user_templates_dir, exist_ok=True)
110
+ # Copy NPCs from package to user directory
111
+ for filename in os.listdir(package_npc_team_dir):
112
+ if filename.endswith(".npc"):
113
+ source_path = os.path.join(package_npc_team_dir, filename)
114
+ destination_path = os.path.join(user_npc_team_dir, filename)
115
+ if not os.path.exists(destination_path) or file_has_changed(
116
+ source_path, destination_path
117
+ ):
118
+ shutil.copy2(source_path, destination_path)
119
+ print(f"Copied NPC {filename} to {destination_path}")
120
+ if filename.endswith(".ctx"):
121
+ source_path = os.path.join(package_npc_team_dir, filename)
122
+ destination_path = os.path.join(user_npc_team_dir, filename)
123
+ if not os.path.exists(destination_path) or file_has_changed(
124
+ source_path, destination_path
125
+ ):
126
+ shutil.copy2(source_path, destination_path)
127
+ print(f"Copied ctx {filename} to {destination_path}")
128
+
129
+ # Copy jinxs from package to user directory
130
+ package_jinxs_dir = os.path.join(package_npc_team_dir, "jinxs")
131
+ if os.path.exists(package_jinxs_dir):
132
+ for filename in os.listdir(package_jinxs_dir):
133
+ if filename.endswith(".jinx"):
134
+ source_jinx_path = os.path.join(package_jinxs_dir, filename)
135
+ destination_jinx_path = os.path.join(user_jinxs_dir, filename)
136
+ if (not os.path.exists(destination_jinx_path)) or file_has_changed(
137
+ source_jinx_path, destination_jinx_path
138
+ ):
139
+ shutil.copy2(source_jinx_path, destination_jinx_path)
140
+ print(f"Copied jinx {filename} to {destination_jinx_path}")
141
+
142
+ templates = os.path.join(package_npc_team_dir, "templates")
143
+ if os.path.exists(templates):
144
+ for folder in os.listdir(templates):
145
+ os.makedirs(os.path.join(user_templates_dir, folder), exist_ok=True)
146
+ for file in os.listdir(os.path.join(templates, folder)):
147
+ if file.endswith(".npc"):
148
+ source_template_path = os.path.join(templates, folder, file)
149
+
150
+ destination_template_path = os.path.join(
151
+ user_templates_dir, folder, file
152
+ )
153
+ if not os.path.exists(
154
+ destination_template_path
155
+ ) or file_has_changed(
156
+ source_template_path, destination_template_path
157
+ ):
158
+ shutil.copy2(source_template_path, destination_template_path)
159
+ print(f"Copied template {file} to {destination_template_path}")
160
+ conn.commit()
161
+ conn.close()
162
+ set_npcsh_initialized()
163
+ add_npcshrc_to_shell_config()
164
+
165
+ def get_shell_config_file() -> str:
166
+ """
167
+
168
+ Function Description:
169
+ This function returns the path to the shell configuration file.
170
+ Args:
171
+ None
172
+ Keyword Args:
173
+ None
174
+ Returns:
175
+ The path to the shell configuration file.
176
+ """
177
+ # Check the current shell
178
+ shell = os.environ.get("SHELL", "")
179
+
180
+ if "zsh" in shell:
181
+ return os.path.expanduser("~/.zshrc")
182
+ elif "bash" in shell:
183
+ # On macOS, use .bash_profile for login shells
184
+ if platform.system() == "Darwin":
185
+ return os.path.expanduser("~/.bash_profile")
186
+ else:
187
+ return os.path.expanduser("~/.bashrc")
188
+ else:
189
+ # Default to .bashrc if we can't determine the shell
190
+ return os.path.expanduser("~/.bashrc")
191
+
192
+
193
+
194
+ def add_npcshrc_to_shell_config() -> None:
195
+ """
196
+ Function Description:
197
+ This function adds the sourcing of the .npcshrc file to the user's shell configuration file.
198
+ Args:
199
+ None
200
+ Keyword Args:
201
+ None
202
+ Returns:
203
+ None
204
+ """
205
+
206
+ if os.getenv("NPCSH_INITIALIZED") is not None:
207
+ return
208
+ config_file = get_shell_config_file()
209
+ npcshrc_line = "\n# Source NPCSH configuration\nif [ -f ~/.npcshrc ]; then\n . ~/.npcshrc\nfi\n"
210
+
211
+ with open(config_file, "a+") as shell_config:
212
+ shell_config.seek(0)
213
+ content = shell_config.read()
214
+ if "source ~/.npcshrc" not in content and ". ~/.npcshrc" not in content:
215
+ shell_config.write(npcshrc_line)
216
+ print(f"Added .npcshrc sourcing to {config_file}")
217
+ else:
218
+ print(f".npcshrc already sourced in {config_file}")
219
+
220
+ def ensure_npcshrc_exists() -> str:
221
+ """
222
+ Function Description:
223
+ This function ensures that the .npcshrc file exists in the user's home directory.
224
+ Args:
225
+ None
226
+ Keyword Args:
227
+ None
228
+ Returns:
229
+ The path to the .npcshrc file.
230
+ """
231
+
232
+ npcshrc_path = os.path.expanduser("~/.npcshrc")
233
+ if not os.path.exists(npcshrc_path):
234
+ with open(npcshrc_path, "w") as npcshrc:
235
+ npcshrc.write("# NPCSH Configuration File\n")
236
+ npcshrc.write("export NPCSH_INITIALIZED=0\n")
237
+ npcshrc.write("export NPCSH_DEFAULT_MODE='agent'\n")
238
+ npcshrc.write("export NPCSH_CHAT_PROVIDER='ollama'\n")
239
+ npcshrc.write("export NPCSH_CHAT_MODEL='llama3.2'\n")
240
+ npcshrc.write("export NPCSH_REASONING_PROVIDER='ollama'\n")
241
+ npcshrc.write("export NPCSH_REASONING_MODEL='deepseek-r1'\n")
242
+
243
+ npcshrc.write("export NPCSH_EMBEDDING_PROVIDER='ollama'\n")
244
+ npcshrc.write("export NPCSH_EMBEDDING_MODEL='nomic-embed-text'\n")
245
+ npcshrc.write("export NPCSH_VISION_PROVIDER='ollama'\n")
246
+ npcshrc.write("export NPCSH_VISION_MODEL='llava7b'\n")
247
+ npcshrc.write(
248
+ "export NPCSH_IMAGE_GEN_MODEL='runwayml/stable-diffusion-v1-5'\n"
249
+ )
250
+
251
+ npcshrc.write("export NPCSH_IMAGE_GEN_PROVIDER='diffusers'\n")
252
+ npcshrc.write(
253
+ "export NPCSH_VIDEO_GEN_MODEL='runwayml/stable-diffusion-v1-5'\n"
254
+ )
255
+
256
+ npcshrc.write("export NPCSH_VIDEO_GEN_PROVIDER='diffusers'\n")
257
+
258
+ npcshrc.write("export NPCSH_API_URL=''\n")
259
+ npcshrc.write("export NPCSH_DB_PATH='~/npcsh_history.db'\n")
260
+ npcshrc.write("export NPCSH_VECTOR_DB_PATH='~/npcsh_chroma.db'\n")
261
+ npcshrc.write("export NPCSH_STREAM_OUTPUT=0")
262
+ return npcshrc_path
263
+
264
+
265
+
266
+ def setup_npcsh_config() -> None:
267
+ """
268
+ Function Description:
269
+ This function initializes the NPCSH configuration.
270
+ Args:
271
+ None
272
+ Keyword Args:
273
+ None
274
+ Returns:
275
+ None
276
+ """
277
+
278
+ ensure_npcshrc_exists()
279
+ add_npcshrc_to_shell_config()
280
+
281
+
282
+ BASH_COMMANDS = [
283
+ "npc",
284
+ "npm",
285
+ "npx",
286
+ "open",
287
+ "alias",
288
+ "bg",
289
+ "bind",
290
+ "break",
291
+ "builtin",
292
+ "case",
293
+ "command",
294
+ "compgen",
295
+ "complete",
296
+ "continue",
297
+ "declare",
298
+ "dirs",
299
+ "disown",
300
+ "echo",
301
+ "enable",
302
+ "eval",
303
+ "exec",
304
+ "exit",
305
+ "export",
306
+ "fc",
307
+ "fg",
308
+ "getopts",
309
+ "hash",
310
+ "help",
311
+ "history",
312
+ "if",
313
+ "jobs",
314
+ "kill",
315
+ "let",
316
+ "local",
317
+ "logout",
318
+ "ollama",
319
+ "popd",
320
+ "printf",
321
+ "pushd",
322
+ "pwd",
323
+ "read",
324
+ "readonly",
325
+ "return",
326
+ "set",
327
+ "shift",
328
+ "shopt",
329
+ "source",
330
+ "suspend",
331
+ "test",
332
+ "times",
333
+ "trap",
334
+ "type",
335
+ "typeset",
336
+ "ulimit",
337
+ "umask",
338
+ "unalias",
339
+ "unset",
340
+ "until",
341
+ "wait",
342
+ "while",
343
+ # Common Unix commands
344
+ "ls",
345
+ "cp",
346
+ "mv",
347
+ "rm",
348
+ "mkdir",
349
+ "rmdir",
350
+ "touch",
351
+ "cat",
352
+ "less",
353
+ "more",
354
+ "head",
355
+ "tail",
356
+ "grep",
357
+ "find",
358
+ "sed",
359
+ "awk",
360
+ "sort",
361
+ "uniq",
362
+ "wc",
363
+ "diff",
364
+ "chmod",
365
+ "chown",
366
+ "chgrp",
367
+ "ln",
368
+ "tar",
369
+ "gzip",
370
+ "gunzip",
371
+ "zip",
372
+ "unzip",
373
+ "ssh",
374
+ "scp",
375
+ "rsync",
376
+ "wget",
377
+ "curl",
378
+ "ping",
379
+ "netstat",
380
+ "ifconfig",
381
+ "route",
382
+ "traceroute",
383
+ "ps",
384
+ "top",
385
+ "htop",
386
+ "kill",
387
+ "killall",
388
+ "su",
389
+ "sudo",
390
+ "whoami",
391
+ "who",
392
+ "w",
393
+ "last",
394
+ "finger",
395
+ "uptime",
396
+ "free",
397
+ "df",
398
+ "du",
399
+ "mount",
400
+ "umount",
401
+ "fdisk",
402
+ "mkfs",
403
+ "fsck",
404
+ "dd",
405
+ "cron",
406
+ "at",
407
+ "systemctl",
408
+ "service",
409
+ "journalctl",
410
+ "man",
411
+ "info",
412
+ "whatis",
413
+ "whereis",
414
+ "which",
415
+ "date",
416
+ "cal",
417
+ "bc",
418
+ "expr",
419
+ "screen",
420
+ "tmux",
421
+ "git",
422
+ "vim",
423
+ "emacs",
424
+ "nano",
425
+ "pip",
426
+ ]
427
+
428
+
429
+ interactive_commands = {
430
+ "ipython": ["ipython"],
431
+ "python": ["python", "-i"],
432
+ "sqlite3": ["sqlite3"],
433
+ "r": ["R", "--interactive"],
434
+ }
435
+
436
+
437
+ def start_interactive_session(command: list) -> int:
438
+ """
439
+ Starts an interactive session. Only works on Unix. On Windows, print a message and return 1.
440
+ """
441
+ if ON_WINDOWS or termios is None or tty is None or pty is None or select is None or signal is None:
442
+ print("Interactive terminal sessions are not supported on Windows.")
443
+ return 1
444
+ # Save the current terminal settings
445
+ old_tty = termios.tcgetattr(sys.stdin)
446
+ try:
447
+ # Create a pseudo-terminal
448
+ master_fd, slave_fd = pty.openpty()
449
+
450
+ # Start the process
451
+ p = subprocess.Popen(
452
+ command,
453
+ stdin=slave_fd,
454
+ stdout=slave_fd,
455
+ stderr=slave_fd,
456
+ shell=True,
457
+ preexec_fn=os.setsid, # Create a new process group
458
+ )
459
+
460
+ # Set the terminal to raw mode
461
+ tty.setraw(sys.stdin.fileno())
462
+
463
+ def handle_timeout(signum, frame):
464
+ raise TimeoutError("Process did not terminate in time")
465
+
466
+ while p.poll() is None:
467
+ r, w, e = select.select([sys.stdin, master_fd], [], [], 0.1)
468
+ if sys.stdin in r:
469
+ d = os.read(sys.stdin.fileno(), 10240)
470
+ os.write(master_fd, d)
471
+ elif master_fd in r:
472
+ o = os.read(master_fd, 10240)
473
+ if o:
474
+ os.write(sys.stdout.fileno(), o)
475
+ else:
476
+ break
477
+
478
+ # Wait for the process to terminate with a timeout
479
+ signal.signal(signal.SIGALRM, handle_timeout)
480
+ signal.alarm(5) # 5 second timeout
481
+ try:
482
+ p.wait()
483
+ except TimeoutError:
484
+ print("\nProcess did not terminate. Force killing...")
485
+ os.killpg(os.getpgid(p.pid), signal.SIGTERM)
486
+ time.sleep(1)
487
+ if p.poll() is None:
488
+ os.killpg(os.getpgid(p.pid), signal.SIGKILL)
489
+ finally:
490
+ signal.alarm(0)
491
+
492
+ finally:
493
+ # Restore the terminal settings
494
+ termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, old_tty)
495
+
496
+ return p.returncode
497
+
498
+ def validate_bash_command(command_parts: list) -> bool:
499
+ """
500
+ Function Description:
501
+ Validate if the command sequence is a valid bash command with proper arguments/flags.
502
+ Args:
503
+ command_parts : list : Command parts
504
+ Keyword Args:
505
+ None
506
+ Returns:
507
+ bool : bool : Boolean
508
+ """
509
+ if not command_parts:
510
+ return False
511
+
512
+ COMMAND_PATTERNS = {
513
+ "cat": {
514
+ "flags": ["-n", "-b", "-E", "-T", "-s", "--number", "-A", "--show-all"],
515
+ "requires_arg": True,
516
+ },
517
+ "find": {
518
+ "flags": [
519
+ "-name",
520
+ "-type",
521
+ "-size",
522
+ "-mtime",
523
+ "-exec",
524
+ "-print",
525
+ "-delete",
526
+ "-maxdepth",
527
+ "-mindepth",
528
+ "-perm",
529
+ "-user",
530
+ "-group",
531
+ ],
532
+ "requires_arg": True,
533
+ },
534
+ "who": {
535
+ "flags": [
536
+ "-a",
537
+ "-b",
538
+ "-d",
539
+ "-H",
540
+ "-l",
541
+ "-p",
542
+ "-q",
543
+ "-r",
544
+ "-s",
545
+ "-t",
546
+ "-u",
547
+ "--all",
548
+ "--count",
549
+ "--heading",
550
+ ],
551
+ "requires_arg": True,
552
+ },
553
+ "open": {
554
+ "flags": ["-a", "-e", "-t", "-f", "-F", "-W", "-n", "-g", "-h"],
555
+ "requires_arg": True,
556
+ },
557
+ "which": {"flags": ["-a", "-s", "-v"], "requires_arg": True},
558
+ }
559
+
560
+ base_command = command_parts[0]
561
+
562
+ if base_command not in COMMAND_PATTERNS:
563
+ return True # Allow other commands to pass through
564
+
565
+ pattern = COMMAND_PATTERNS[base_command]
566
+ args = []
567
+ flags = []
568
+
569
+ for i in range(1, len(command_parts)):
570
+ part = command_parts[i]
571
+ if part.startswith("-"):
572
+ flags.append(part)
573
+ if part not in pattern["flags"]:
574
+ return False # Invalid flag
575
+ else:
576
+ args.append(part)
577
+
578
+ # Check if 'who' has any arguments (it shouldn't)
579
+ if base_command == "who" and args:
580
+ return False
581
+
582
+ # Handle 'which' with '-a' flag
583
+ if base_command == "which" and "-a" in flags:
584
+ return True # Allow 'which -a' with or without arguments.
585
+
586
+ # Check if any required arguments are missing
587
+ if pattern.get("requires_arg", False) and not args:
588
+ return False
589
+
590
+ return True
591
+
592
+
593
+ def is_npcsh_initialized() -> bool:
594
+ """
595
+ Function Description:
596
+ This function checks if the NPCSH initialization flag is set.
597
+ Args:
598
+ None
599
+ Keyword Args:
600
+ None
601
+ Returns:
602
+ A boolean indicating whether NPCSH is initialized.
603
+ """
604
+
605
+ return os.environ.get("NPCSH_INITIALIZED", None) == "1"
606
+
607
+
608
+ def execute_set_command(command: str, value: str) -> str:
609
+ """
610
+ Function Description:
611
+ This function sets a configuration value in the .npcshrc file.
612
+ Args:
613
+ command: The command to execute.
614
+ value: The value to set.
615
+ Keyword Args:
616
+ None
617
+ Returns:
618
+ A message indicating the success or failure of the operation.
619
+ """
620
+
621
+ config_path = os.path.expanduser("~/.npcshrc")
622
+
623
+ # Map command to environment variable name
624
+ var_map = {
625
+ "model": "NPCSH_CHAT_MODEL",
626
+ "provider": "NPCSH_CHAT_PROVIDER",
627
+ "db_path": "NPCSH_DB_PATH",
628
+ }
629
+
630
+ if command not in var_map:
631
+ return f"Unknown setting: {command}"
632
+
633
+ env_var = var_map[command]
634
+
635
+ # Read the current configuration
636
+ if os.path.exists(config_path):
637
+ with open(config_path, "r") as f:
638
+ lines = f.readlines()
639
+ else:
640
+ lines = []
641
+
642
+ # Check if the property exists and update it, or add it if it doesn't exist
643
+ property_exists = False
644
+ for i, line in enumerate(lines):
645
+ if line.startswith(f"export {env_var}="):
646
+ lines[i] = f"export {env_var}='{value}'\n"
647
+ property_exists = True
648
+ break
649
+
650
+ if not property_exists:
651
+ lines.append(f"export {env_var}='{value}'\n")
652
+
653
+ # Save the updated configuration
654
+ with open(config_path, "w") as f:
655
+ f.writelines(lines)
656
+
657
+ return f"{command.capitalize()} has been set to: {value}"
658
+
659
+
660
+ def set_npcsh_initialized() -> None:
661
+ """
662
+ Function Description:
663
+ This function sets the NPCSH initialization flag in the .npcshrc file.
664
+ Args:
665
+ None
666
+ Keyword Args:
667
+ None
668
+ Returns:
669
+
670
+ None
671
+ """
672
+
673
+ npcshrc_path = ensure_npcshrc_exists()
674
+
675
+ with open(npcshrc_path, "r+") as npcshrc:
676
+ content = npcshrc.read()
677
+ if "export NPCSH_INITIALIZED=0" in content:
678
+ content = content.replace(
679
+ "export NPCSH_INITIALIZED=0", "export NPCSH_INITIALIZED=1"
680
+ )
681
+ npcshrc.seek(0)
682
+ npcshrc.write(content)
683
+ npcshrc.truncate()
684
+
685
+ # Also set it for the current session
686
+ os.environ["NPCSH_INITIALIZED"] = "1"
687
+ print("NPCSH initialization flag set in .npcshrc")
688
+
689
+
690
+
691
+ def file_has_changed(source_path: str, destination_path: str) -> bool:
692
+ """
693
+ Function Description:
694
+ This function compares two files to determine if they are different.
695
+ Args:
696
+ source_path: The path to the source file.
697
+ destination_path: The path to the destination file.
698
+ Keyword Args:
699
+ None
700
+ Returns:
701
+ A boolean indicating whether the files are different
702
+ """
703
+
704
+ # Compare file modification times or contents to decide whether to update the file
705
+ return not filecmp.cmp(source_path, destination_path, shallow=False)
706
+
707
+
708
+ def list_directory(args: List[str]) -> None:
709
+ """
710
+ Function Description:
711
+ This function lists the contents of a directory.
712
+ Args:
713
+ args: The command arguments.
714
+ Keyword Args:
715
+ None
716
+ Returns:
717
+ None
718
+ """
719
+ directory = args[0] if args else "."
720
+ try:
721
+ files = os.listdir(directory)
722
+ for f in files:
723
+ print(f)
724
+ except Exception as e:
725
+ print(f"Error listing directory: {e}")
726
+
727
+
728
+
729
+ def change_directory(command_parts: list, messages: list) -> dict:
730
+ """
731
+ Function Description:
732
+ Changes the current directory.
733
+ Args:
734
+ command_parts : list : Command parts
735
+ messages : list : Messages
736
+ Keyword Args:
737
+ None
738
+ Returns:
739
+ dict : dict : Dictionary
740
+
741
+ """
742
+
743
+ try:
744
+ if len(command_parts) > 1:
745
+ new_dir = os.path.expanduser(command_parts[1])
746
+ else:
747
+ new_dir = os.path.expanduser("~")
748
+ os.chdir(new_dir)
749
+ return {
750
+ "messages": messages,
751
+ "output": f"Changed directory to {os.getcwd()}",
752
+ }
753
+ except FileNotFoundError:
754
+ return {
755
+ "messages": messages,
756
+ "output": f"Directory not found: {new_dir}",
757
+ }
758
+ except PermissionError:
759
+ return {"messages": messages, "output": f"Permission denied: {new_dir}"}
760
+
761
+
762
+ def orange(text: str) -> str:
763
+ """
764
+ Function Description:
765
+ Returns orange text.
766
+ Args:
767
+ text : str : Text
768
+ Keyword Args:
769
+ None
770
+ Returns:
771
+ text : str : Text
772
+
773
+ """
774
+ return f"\033[38;2;255;165;0m{text}{Style.RESET_ALL}"
775
+
776
+
777
+ def get_npcshrc_path_windows():
778
+ return Path.home() / ".npcshrc"
779
+
780
+
781
+ def read_rc_file_windows(path):
782
+ """Read shell-style rc file"""
783
+ config = {}
784
+ if not path.exists():
785
+ return config
786
+
787
+ with open(path) as f:
788
+ for line in f:
789
+ line = line.strip()
790
+ if line and not line.startswith("#"):
791
+ # Match KEY='value' or KEY="value" format
792
+ match = re.match(r'^([A-Z_]+)\s*=\s*[\'"](.*?)[\'"]$', line)
793
+ if match:
794
+ key, value = match.groups()
795
+ config[key] = value
796
+ return config
797
+
798
+
799
+ def get_setting_windows(key, default=None):
800
+ # Try environment variable first
801
+ if env_value := os.getenv(key):
802
+ return env_value
803
+
804
+ # Fall back to .npcshrc file
805
+ config = read_rc_file_windows(get_npcshrc_path_windows())
806
+ return config.get(key, default)
807
+
808
+ NPCSH_CHAT_MODEL = os.environ.get("NPCSH_CHAT_MODEL", "llama3.2")
809
+ # print("NPCSH_CHAT_MODEL", NPCSH_CHAT_MODEL)
810
+ NPCSH_CHAT_PROVIDER = os.environ.get("NPCSH_CHAT_PROVIDER", "ollama")
811
+ # print("NPCSH_CHAT_PROVIDER", NPCSH_CHAT_PROVIDER)
812
+ NPCSH_DB_PATH = os.path.expanduser(
813
+ os.environ.get("NPCSH_DB_PATH", "~/npcsh_history.db")
814
+ )
815
+ NPCSH_VECTOR_DB_PATH = os.path.expanduser(
816
+ os.environ.get("NPCSH_VECTOR_DB_PATH", "~/npcsh_chroma.db")
817
+ )
818
+ #DEFAULT MODES = ['CHAT', 'AGENT', 'CODE', ]
819
+
820
+ NPCSH_DEFAULT_MODE = os.path.expanduser(os.environ.get("NPCSH_DEFAULT_MODE", "agent"))
821
+ NPCSH_VISION_MODEL = os.environ.get("NPCSH_VISION_MODEL", "llava:7b")
822
+ NPCSH_VISION_PROVIDER = os.environ.get("NPCSH_VISION_PROVIDER", "ollama")
823
+ NPCSH_IMAGE_GEN_MODEL = os.environ.get(
824
+ "NPCSH_IMAGE_GEN_MODEL", "runwayml/stable-diffusion-v1-5"
825
+ )
826
+ NPCSH_IMAGE_GEN_PROVIDER = os.environ.get("NPCSH_IMAGE_GEN_PROVIDER", "diffusers")
827
+ NPCSH_VIDEO_GEN_MODEL = os.environ.get(
828
+ "NPCSH_VIDEO_GEN_MODEL", "damo-vilab/text-to-video-ms-1.7b"
829
+ )
830
+ NPCSH_VIDEO_GEN_PROVIDER = os.environ.get("NPCSH_VIDEO_GEN_PROVIDER", "diffusers")
831
+
832
+ NPCSH_EMBEDDING_MODEL = os.environ.get("NPCSH_EMBEDDING_MODEL", "nomic-embed-text")
833
+ NPCSH_EMBEDDING_PROVIDER = os.environ.get("NPCSH_EMBEDDING_PROVIDER", "ollama")
834
+ NPCSH_REASONING_MODEL = os.environ.get("NPCSH_REASONING_MODEL", "deepseek-r1")
835
+ NPCSH_REASONING_PROVIDER = os.environ.get("NPCSH_REASONING_PROVIDER", "ollama")
836
+ NPCSH_STREAM_OUTPUT = eval(os.environ.get("NPCSH_STREAM_OUTPUT", "0")) == 1
837
+ NPCSH_API_URL = os.environ.get("NPCSH_API_URL", None)
838
+ NPCSH_SEARCH_PROVIDER = os.environ.get("NPCSH_SEARCH_PROVIDER", "duckduckgo")
839
+
840
+ READLINE_HISTORY_FILE = os.path.expanduser("~/.npcsh_history")
841
+
842
+
843
+
844
+ def setup_readline() -> str:
845
+ if readline is None:
846
+ return None
847
+ try:
848
+ readline.read_history_file(READLINE_HISTORY_FILE)
849
+ readline.set_history_length(1000)
850
+ readline.parse_and_bind("set enable-bracketed-paste on")
851
+ readline.parse_and_bind(r'"\e[A": history-search-backward')
852
+ readline.parse_and_bind(r'"\e[B": history-search-forward')
853
+ readline.parse_and_bind(r'"\C-r": reverse-search-history')
854
+ readline.parse_and_bind(r'\C-e: end-of-line')
855
+ readline.parse_and_bind(r'\C-a: beginning-of-line')
856
+ if sys.platform == "darwin":
857
+ readline.parse_and_bind("bind ^I rl_complete")
858
+ else:
859
+ readline.parse_and_bind("tab: complete")
860
+ return READLINE_HISTORY_FILE
861
+ except FileNotFoundError:
862
+ pass
863
+ except OSError as e:
864
+ print(f"Warning: Could not read readline history file {READLINE_HISTORY_FILE}: {e}")
865
+
866
+ def save_readline_history():
867
+ if readline is None:
868
+ return
869
+ try:
870
+ readline.write_history_file(READLINE_HISTORY_FILE)
871
+ except OSError as e:
872
+ print(f"Warning: Could not write readline history file {READLINE_HISTORY_FILE}: {e}")
873
+
874
+
875
+
876
+
877
+
878
+ from npcpy.memory.command_history import (
879
+ start_new_conversation,
880
+ )
881
+ from dataclasses import dataclass, field
882
+ from typing import Optional, List, Dict, Any, Tuple, Union
883
+ from npcpy.npc_compiler import NPC, Team
884
+ import os
885
+ @dataclass
886
+ class ShellState:
887
+ npc: Optional[Union[NPC, str]] = None
888
+ team: Optional[Team] = None
889
+ messages: List[Dict[str, Any]] = field(default_factory=list)
890
+ mcp_client: Optional[Any] = None
891
+ conversation_id: Optional[int] = None
892
+ chat_model: str = NPCSH_CHAT_MODEL
893
+ chat_provider: str = NPCSH_CHAT_PROVIDER
894
+ vision_model: str = NPCSH_VISION_MODEL
895
+ vision_provider: str = NPCSH_VISION_PROVIDER
896
+ embedding_model: str = NPCSH_EMBEDDING_MODEL
897
+ embedding_provider: str = NPCSH_EMBEDDING_PROVIDER
898
+ reasoning_model: str = NPCSH_REASONING_MODEL
899
+ reasoning_provider: str = NPCSH_REASONING_PROVIDER
900
+ image_gen_model: str = NPCSH_IMAGE_GEN_MODEL
901
+ image_gen_provider: str = NPCSH_IMAGE_GEN_PROVIDER
902
+ video_gen_model: str = NPCSH_VIDEO_GEN_MODEL
903
+ video_gen_provider: str = NPCSH_VIDEO_GEN_PROVIDER
904
+ current_mode: str = NPCSH_DEFAULT_MODE
905
+ api_key: Optional[str] = None
906
+ api_url: Optional[str] = NPCSH_API_URL
907
+ current_path: str = field(default_factory=os.getcwd)
908
+ stream_output: bool = NPCSH_STREAM_OUTPUT
909
+ attachments: Optional[List[Any]] = None
910
+ def get_model_for_command(self, model_type: str = "chat"):
911
+ if model_type == "chat":
912
+ return self.chat_model, self.chat_provider
913
+ elif model_type == "vision":
914
+ return self.vision_model, self.vision_provider
915
+ elif model_type == "embedding":
916
+ return self.embedding_model, self.embedding_provider
917
+ elif model_type == "reasoning":
918
+ return self.reasoning_model, self.reasoning_provider
919
+ elif model_type == "image_gen":
920
+ return self.image_gen_model, self.image_gen_provider
921
+ elif model_type == "video_gen":
922
+ return self.video_gen_model, self.video_gen_provider
923
+ else:
924
+ return self.chat_model, self.chat_provider # Default fallback
925
+ initial_state = ShellState(
926
+ conversation_id=start_new_conversation(),
927
+ stream_output=NPCSH_STREAM_OUTPUT,
928
+ current_mode=NPCSH_DEFAULT_MODE,
929
+ chat_model=NPCSH_CHAT_MODEL,
930
+ chat_provider=NPCSH_CHAT_PROVIDER,
931
+ vision_model=NPCSH_VISION_MODEL,
932
+ vision_provider=NPCSH_VISION_PROVIDER,
933
+ embedding_model=NPCSH_EMBEDDING_MODEL,
934
+ embedding_provider=NPCSH_EMBEDDING_PROVIDER,
935
+ reasoning_model=NPCSH_REASONING_MODEL,
936
+ reasoning_provider=NPCSH_REASONING_PROVIDER,
937
+ image_gen_model=NPCSH_IMAGE_GEN_MODEL,
938
+ image_gen_provider=NPCSH_IMAGE_GEN_PROVIDER,
939
+ video_gen_model=NPCSH_VIDEO_GEN_MODEL,
940
+ video_gen_provider=NPCSH_VIDEO_GEN_PROVIDER,
941
+ api_url=NPCSH_API_URL,
942
+ )