npcsh 0.1.2__py3-none-any.whl → 1.1.13__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 (143) hide show
  1. npcsh/_state.py +3508 -0
  2. npcsh/alicanto.py +65 -0
  3. npcsh/build.py +291 -0
  4. npcsh/completion.py +206 -0
  5. npcsh/config.py +163 -0
  6. npcsh/corca.py +50 -0
  7. npcsh/execution.py +185 -0
  8. npcsh/guac.py +46 -0
  9. npcsh/mcp_helpers.py +357 -0
  10. npcsh/mcp_server.py +299 -0
  11. npcsh/npc.py +323 -0
  12. npcsh/npc_team/alicanto.npc +2 -0
  13. npcsh/npc_team/alicanto.png +0 -0
  14. npcsh/npc_team/corca.npc +12 -0
  15. npcsh/npc_team/corca.png +0 -0
  16. npcsh/npc_team/corca_example.png +0 -0
  17. npcsh/npc_team/foreman.npc +7 -0
  18. npcsh/npc_team/frederic.npc +6 -0
  19. npcsh/npc_team/frederic4.png +0 -0
  20. npcsh/npc_team/guac.png +0 -0
  21. npcsh/npc_team/jinxs/code/python.jinx +11 -0
  22. npcsh/npc_team/jinxs/code/sh.jinx +34 -0
  23. npcsh/npc_team/jinxs/code/sql.jinx +16 -0
  24. npcsh/npc_team/jinxs/modes/alicanto.jinx +194 -0
  25. npcsh/npc_team/jinxs/modes/corca.jinx +249 -0
  26. npcsh/npc_team/jinxs/modes/guac.jinx +317 -0
  27. npcsh/npc_team/jinxs/modes/plonk.jinx +214 -0
  28. npcsh/npc_team/jinxs/modes/pti.jinx +170 -0
  29. npcsh/npc_team/jinxs/modes/spool.jinx +161 -0
  30. npcsh/npc_team/jinxs/modes/wander.jinx +186 -0
  31. npcsh/npc_team/jinxs/modes/yap.jinx +262 -0
  32. npcsh/npc_team/jinxs/npc_studio/npc-studio.jinx +77 -0
  33. npcsh/npc_team/jinxs/utils/agent.jinx +17 -0
  34. npcsh/npc_team/jinxs/utils/chat.jinx +44 -0
  35. npcsh/npc_team/jinxs/utils/cmd.jinx +44 -0
  36. npcsh/npc_team/jinxs/utils/compress.jinx +140 -0
  37. npcsh/npc_team/jinxs/utils/core/build.jinx +65 -0
  38. npcsh/npc_team/jinxs/utils/core/compile.jinx +50 -0
  39. npcsh/npc_team/jinxs/utils/core/help.jinx +52 -0
  40. npcsh/npc_team/jinxs/utils/core/init.jinx +41 -0
  41. npcsh/npc_team/jinxs/utils/core/jinxs.jinx +32 -0
  42. npcsh/npc_team/jinxs/utils/core/set.jinx +40 -0
  43. npcsh/npc_team/jinxs/utils/edit_file.jinx +94 -0
  44. npcsh/npc_team/jinxs/utils/load_file.jinx +35 -0
  45. npcsh/npc_team/jinxs/utils/ots.jinx +61 -0
  46. npcsh/npc_team/jinxs/utils/roll.jinx +68 -0
  47. npcsh/npc_team/jinxs/utils/sample.jinx +56 -0
  48. npcsh/npc_team/jinxs/utils/search.jinx +130 -0
  49. npcsh/npc_team/jinxs/utils/serve.jinx +26 -0
  50. npcsh/npc_team/jinxs/utils/sleep.jinx +116 -0
  51. npcsh/npc_team/jinxs/utils/trigger.jinx +61 -0
  52. npcsh/npc_team/jinxs/utils/usage.jinx +33 -0
  53. npcsh/npc_team/jinxs/utils/vixynt.jinx +144 -0
  54. npcsh/npc_team/kadiefa.npc +3 -0
  55. npcsh/npc_team/kadiefa.png +0 -0
  56. npcsh/npc_team/npcsh.ctx +18 -0
  57. npcsh/npc_team/npcsh_sibiji.png +0 -0
  58. npcsh/npc_team/plonk.npc +2 -0
  59. npcsh/npc_team/plonk.png +0 -0
  60. npcsh/npc_team/plonkjr.npc +2 -0
  61. npcsh/npc_team/plonkjr.png +0 -0
  62. npcsh/npc_team/sibiji.npc +3 -0
  63. npcsh/npc_team/sibiji.png +0 -0
  64. npcsh/npc_team/spool.png +0 -0
  65. npcsh/npc_team/yap.png +0 -0
  66. npcsh/npcsh.py +296 -112
  67. npcsh/parsing.py +118 -0
  68. npcsh/plonk.py +54 -0
  69. npcsh/pti.py +54 -0
  70. npcsh/routes.py +139 -0
  71. npcsh/spool.py +48 -0
  72. npcsh/ui.py +199 -0
  73. npcsh/wander.py +62 -0
  74. npcsh/yap.py +50 -0
  75. npcsh-1.1.13.data/data/npcsh/npc_team/agent.jinx +17 -0
  76. npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.jinx +194 -0
  77. npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.npc +2 -0
  78. npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.png +0 -0
  79. npcsh-1.1.13.data/data/npcsh/npc_team/build.jinx +65 -0
  80. npcsh-1.1.13.data/data/npcsh/npc_team/chat.jinx +44 -0
  81. npcsh-1.1.13.data/data/npcsh/npc_team/cmd.jinx +44 -0
  82. npcsh-1.1.13.data/data/npcsh/npc_team/compile.jinx +50 -0
  83. npcsh-1.1.13.data/data/npcsh/npc_team/compress.jinx +140 -0
  84. npcsh-1.1.13.data/data/npcsh/npc_team/corca.jinx +249 -0
  85. npcsh-1.1.13.data/data/npcsh/npc_team/corca.npc +12 -0
  86. npcsh-1.1.13.data/data/npcsh/npc_team/corca.png +0 -0
  87. npcsh-1.1.13.data/data/npcsh/npc_team/corca_example.png +0 -0
  88. npcsh-1.1.13.data/data/npcsh/npc_team/edit_file.jinx +94 -0
  89. npcsh-1.1.13.data/data/npcsh/npc_team/foreman.npc +7 -0
  90. npcsh-1.1.13.data/data/npcsh/npc_team/frederic.npc +6 -0
  91. npcsh-1.1.13.data/data/npcsh/npc_team/frederic4.png +0 -0
  92. npcsh-1.1.13.data/data/npcsh/npc_team/guac.jinx +317 -0
  93. npcsh-1.1.13.data/data/npcsh/npc_team/guac.png +0 -0
  94. npcsh-1.1.13.data/data/npcsh/npc_team/help.jinx +52 -0
  95. npcsh-1.1.13.data/data/npcsh/npc_team/init.jinx +41 -0
  96. npcsh-1.1.13.data/data/npcsh/npc_team/jinxs.jinx +32 -0
  97. npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.npc +3 -0
  98. npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.png +0 -0
  99. npcsh-1.1.13.data/data/npcsh/npc_team/load_file.jinx +35 -0
  100. npcsh-1.1.13.data/data/npcsh/npc_team/npc-studio.jinx +77 -0
  101. npcsh-1.1.13.data/data/npcsh/npc_team/npcsh.ctx +18 -0
  102. npcsh-1.1.13.data/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  103. npcsh-1.1.13.data/data/npcsh/npc_team/ots.jinx +61 -0
  104. npcsh-1.1.13.data/data/npcsh/npc_team/plonk.jinx +214 -0
  105. npcsh-1.1.13.data/data/npcsh/npc_team/plonk.npc +2 -0
  106. npcsh-1.1.13.data/data/npcsh/npc_team/plonk.png +0 -0
  107. npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.npc +2 -0
  108. npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.png +0 -0
  109. npcsh-1.1.13.data/data/npcsh/npc_team/pti.jinx +170 -0
  110. npcsh-1.1.13.data/data/npcsh/npc_team/python.jinx +11 -0
  111. npcsh-1.1.13.data/data/npcsh/npc_team/roll.jinx +68 -0
  112. npcsh-1.1.13.data/data/npcsh/npc_team/sample.jinx +56 -0
  113. npcsh-1.1.13.data/data/npcsh/npc_team/search.jinx +130 -0
  114. npcsh-1.1.13.data/data/npcsh/npc_team/serve.jinx +26 -0
  115. npcsh-1.1.13.data/data/npcsh/npc_team/set.jinx +40 -0
  116. npcsh-1.1.13.data/data/npcsh/npc_team/sh.jinx +34 -0
  117. npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.npc +3 -0
  118. npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.png +0 -0
  119. npcsh-1.1.13.data/data/npcsh/npc_team/sleep.jinx +116 -0
  120. npcsh-1.1.13.data/data/npcsh/npc_team/spool.jinx +161 -0
  121. npcsh-1.1.13.data/data/npcsh/npc_team/spool.png +0 -0
  122. npcsh-1.1.13.data/data/npcsh/npc_team/sql.jinx +16 -0
  123. npcsh-1.1.13.data/data/npcsh/npc_team/trigger.jinx +61 -0
  124. npcsh-1.1.13.data/data/npcsh/npc_team/usage.jinx +33 -0
  125. npcsh-1.1.13.data/data/npcsh/npc_team/vixynt.jinx +144 -0
  126. npcsh-1.1.13.data/data/npcsh/npc_team/wander.jinx +186 -0
  127. npcsh-1.1.13.data/data/npcsh/npc_team/yap.jinx +262 -0
  128. npcsh-1.1.13.data/data/npcsh/npc_team/yap.png +0 -0
  129. npcsh-1.1.13.dist-info/METADATA +522 -0
  130. npcsh-1.1.13.dist-info/RECORD +135 -0
  131. {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info}/WHEEL +1 -1
  132. npcsh-1.1.13.dist-info/entry_points.txt +9 -0
  133. {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info/licenses}/LICENSE +1 -1
  134. npcsh/command_history.py +0 -81
  135. npcsh/helpers.py +0 -36
  136. npcsh/llm_funcs.py +0 -295
  137. npcsh/main.py +0 -5
  138. npcsh/modes.py +0 -343
  139. npcsh/npc_compiler.py +0 -124
  140. npcsh-0.1.2.dist-info/METADATA +0 -99
  141. npcsh-0.1.2.dist-info/RECORD +0 -14
  142. npcsh-0.1.2.dist-info/entry_points.txt +0 -2
  143. {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info}/top_level.txt +0 -0
npcsh/config.py ADDED
@@ -0,0 +1,163 @@
1
+ """
2
+ npcsh configuration management
3
+ """
4
+ import os
5
+ import importlib.metadata
6
+ from typing import Optional, Dict, Any
7
+
8
+ # Version
9
+ try:
10
+ VERSION = importlib.metadata.version("npcsh")
11
+ except importlib.metadata.PackageNotFoundError:
12
+ VERSION = "unknown"
13
+
14
+ # Default paths
15
+ DEFAULT_NPC_TEAM_PATH = "~/.npcsh/npc_team"
16
+ PROJECT_NPC_TEAM_PATH = "./npc_team"
17
+ HISTORY_DB_DEFAULT_PATH = "~/.npcsh_history.db"
18
+ READLINE_HISTORY_FILE = os.path.expanduser("~/.npcsh_history")
19
+
20
+ # Environment defaults
21
+ NPCSH_CHAT_MODEL = os.environ.get("NPCSH_CHAT_MODEL", "gemma3:4b")
22
+ NPCSH_CHAT_PROVIDER = os.environ.get("NPCSH_CHAT_PROVIDER", "ollama")
23
+ NPCSH_DB_PATH = os.path.expanduser(
24
+ os.environ.get("NPCSH_DB_PATH", "~/npcsh_history.db")
25
+ )
26
+ NPCSH_VECTOR_DB_PATH = os.path.expanduser(
27
+ os.environ.get("NPCSH_VECTOR_DB_PATH", "~/npcsh_chroma.db")
28
+ )
29
+ NPCSH_DEFAULT_MODE = os.environ.get("NPCSH_DEFAULT_MODE", "agent")
30
+ NPCSH_VISION_MODEL = os.environ.get("NPCSH_VISION_MODEL", "gemma3:4b")
31
+ NPCSH_VISION_PROVIDER = os.environ.get("NPCSH_VISION_PROVIDER", "ollama")
32
+ NPCSH_IMAGE_GEN_MODEL = os.environ.get(
33
+ "NPCSH_IMAGE_GEN_MODEL", "runwayml/stable-diffusion-v1-5"
34
+ )
35
+ NPCSH_IMAGE_GEN_PROVIDER = os.environ.get("NPCSH_IMAGE_GEN_PROVIDER", "diffusers")
36
+ NPCSH_VIDEO_GEN_MODEL = os.environ.get(
37
+ "NPCSH_VIDEO_GEN_MODEL", "damo-vilab/text-to-video-ms-1.7b"
38
+ )
39
+ NPCSH_VIDEO_GEN_PROVIDER = os.environ.get("NPCSH_VIDEO_GEN_PROVIDER", "diffusers")
40
+ NPCSH_EMBEDDING_MODEL = os.environ.get("NPCSH_EMBEDDING_MODEL", "nomic-embed-text")
41
+ NPCSH_EMBEDDING_PROVIDER = os.environ.get("NPCSH_EMBEDDING_PROVIDER", "ollama")
42
+ NPCSH_REASONING_MODEL = os.environ.get("NPCSH_REASONING_MODEL", "deepseek-r1")
43
+ NPCSH_REASONING_PROVIDER = os.environ.get("NPCSH_REASONING_PROVIDER", "ollama")
44
+ NPCSH_STREAM_OUTPUT = os.environ.get("NPCSH_STREAM_OUTPUT", "0") == "1"
45
+ NPCSH_API_URL = os.environ.get("NPCSH_API_URL", None)
46
+ NPCSH_SEARCH_PROVIDER = os.environ.get("NPCSH_SEARCH_PROVIDER", "duckduckgo")
47
+ NPCSH_BUILD_KG = os.environ.get("NPCSH_BUILD_KG") == "1"
48
+
49
+
50
+ def get_shell_config_file() -> str:
51
+ """Get the path to the user's shell config file"""
52
+ shell = os.environ.get("SHELL", "/bin/bash")
53
+
54
+ if "zsh" in shell:
55
+ return os.path.expanduser("~/.zshrc")
56
+ elif "fish" in shell:
57
+ return os.path.expanduser("~/.config/fish/config.fish")
58
+ else:
59
+ return os.path.expanduser("~/.bashrc")
60
+
61
+
62
+ def get_npcshrc_path() -> str:
63
+ """Get path to npcshrc file"""
64
+ return os.path.expanduser("~/.npcshrc")
65
+
66
+
67
+ def get_npcshrc_path_windows():
68
+ """Get npcshrc path on Windows"""
69
+ return os.path.expanduser("~/.npcshrc")
70
+
71
+
72
+ def ensure_npcshrc_exists() -> str:
73
+ """Ensure npcshrc file exists and return its path"""
74
+ npcshrc_path = get_npcshrc_path()
75
+
76
+ if not os.path.exists(npcshrc_path):
77
+ default_content = f"""# npcsh configuration file
78
+ export NPCSH_CHAT_MODEL="{NPCSH_CHAT_MODEL}"
79
+ export NPCSH_CHAT_PROVIDER="{NPCSH_CHAT_PROVIDER}"
80
+ export NPCSH_VISION_MODEL="{NPCSH_VISION_MODEL}"
81
+ export NPCSH_VISION_PROVIDER="{NPCSH_VISION_PROVIDER}"
82
+ export NPCSH_EMBEDDING_MODEL="{NPCSH_EMBEDDING_MODEL}"
83
+ export NPCSH_EMBEDDING_PROVIDER="{NPCSH_EMBEDDING_PROVIDER}"
84
+ export NPCSH_SEARCH_PROVIDER="{NPCSH_SEARCH_PROVIDER}"
85
+ export NPCSH_DEFAULT_MODE="{NPCSH_DEFAULT_MODE}"
86
+ export NPCSH_STREAM_OUTPUT="0"
87
+ """
88
+ with open(npcshrc_path, 'w') as f:
89
+ f.write(default_content)
90
+
91
+ return npcshrc_path
92
+
93
+
94
+ def add_npcshrc_to_shell_config() -> None:
95
+ """Add sourcing of npcshrc to shell config if not present"""
96
+ shell_config = get_shell_config_file()
97
+ npcshrc_path = get_npcshrc_path()
98
+
99
+ source_line = f'source "{npcshrc_path}"'
100
+
101
+ if os.path.exists(shell_config):
102
+ with open(shell_config, 'r') as f:
103
+ content = f.read()
104
+ if npcshrc_path not in content and '.npcshrc' not in content:
105
+ with open(shell_config, 'a') as f:
106
+ f.write(f"\n# Source npcsh configuration\n{source_line}\n")
107
+
108
+
109
+ def setup_npcsh_config() -> None:
110
+ """Set up npcsh configuration"""
111
+ ensure_npcshrc_exists()
112
+ add_npcshrc_to_shell_config()
113
+
114
+
115
+ def is_npcsh_initialized() -> bool:
116
+ """Check if npcsh has been initialized"""
117
+ marker = os.path.expanduser("~/.npcsh/.initialized")
118
+ return os.path.exists(marker)
119
+
120
+
121
+ def set_npcsh_initialized() -> None:
122
+ """Mark npcsh as initialized"""
123
+ npcsh_dir = os.path.expanduser("~/.npcsh")
124
+ os.makedirs(npcsh_dir, exist_ok=True)
125
+ marker = os.path.join(npcsh_dir, ".initialized")
126
+ with open(marker, 'w') as f:
127
+ f.write("1")
128
+
129
+
130
+ def set_npcsh_config_value(key: str, value: str) -> None:
131
+ """Set a value in npcshrc"""
132
+ npcshrc_path = ensure_npcshrc_exists()
133
+
134
+ with open(npcshrc_path, 'r') as f:
135
+ lines = f.readlines()
136
+
137
+ found = False
138
+ for i, line in enumerate(lines):
139
+ if line.strip().startswith(f'export {key}='):
140
+ lines[i] = f'export {key}="{value}"\n'
141
+ found = True
142
+ break
143
+
144
+ if not found:
145
+ lines.append(f'export {key}="{value}"\n')
146
+
147
+ with open(npcshrc_path, 'w') as f:
148
+ f.writelines(lines)
149
+
150
+ # Also set in current environment
151
+ os.environ[key] = value
152
+
153
+
154
+ def get_setting_windows(key, default=None):
155
+ """Get setting on Windows"""
156
+ npcshrc_path = get_npcshrc_path_windows()
157
+ if os.path.exists(npcshrc_path):
158
+ with open(npcshrc_path, 'r') as f:
159
+ for line in f:
160
+ if line.strip().startswith(f'export {key}='):
161
+ value = line.split('=', 1)[1].strip().strip('"').strip("'")
162
+ return value
163
+ return default
npcsh/corca.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ corca - MCP-powered agentic shell CLI entry point
3
+
4
+ This is a thin wrapper that executes the corca.jinx through the jinx mechanism.
5
+ """
6
+ import argparse
7
+ import os
8
+ import sys
9
+
10
+ from npcsh._state import setup_shell
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(description="corca - MCP-powered agentic shell")
15
+ parser.add_argument("command", nargs="*", help="Optional one-shot command to execute")
16
+ parser.add_argument("--model", "-m", type=str, help="LLM model to use")
17
+ parser.add_argument("--provider", "-p", type=str, help="LLM provider to use")
18
+ parser.add_argument("--mcp-server", type=str, help="Path to MCP server script")
19
+ args = parser.parse_args()
20
+
21
+ # Setup shell to get team and default NPC
22
+ command_history, team, default_npc = setup_shell()
23
+
24
+ if not team or "corca" not in team.jinxs_dict:
25
+ print("Error: corca jinx not found. Ensure npc_team/jinxs/modes/corca.jinx exists.")
26
+ sys.exit(1)
27
+
28
+ # Build context for jinx execution
29
+ initial_command = " ".join(args.command) if args.command else None
30
+
31
+ context = {
32
+ "npc": default_npc,
33
+ "team": team,
34
+ "messages": [],
35
+ "model": args.model,
36
+ "provider": args.provider,
37
+ "mcp_server_path": args.mcp_server,
38
+ "initial_command": initial_command,
39
+ }
40
+
41
+ # Execute the jinx
42
+ corca_jinx = team.jinxs_dict["corca"]
43
+ result = corca_jinx.execute(context=context, npc=default_npc)
44
+
45
+ if isinstance(result, dict) and result.get("output"):
46
+ print(result["output"])
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
npcsh/execution.py ADDED
@@ -0,0 +1,185 @@
1
+ """
2
+ Command execution utilities for npcsh
3
+ """
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ from typing import List, Tuple, Any, Optional
9
+
10
+ from termcolor import colored
11
+
12
+
13
+ # Commands that require interactive terminal handling
14
+ TERMINAL_EDITORS = ['vim', 'nvim', 'nano', 'vi', 'emacs', 'less', 'more', 'man']
15
+
16
+ # Interactive commands that need special handling (command -> args)
17
+ INTERACTIVE_COMMANDS = {
18
+ 'ipython': ['ipython'],
19
+ 'python': ['python', '-i'],
20
+ 'python3': ['python3', '-i'],
21
+ 'node': ['node'],
22
+ 'irb': ['irb'],
23
+ 'ghci': ['ghci'],
24
+ 'mysql': ['mysql'],
25
+ 'psql': ['psql'],
26
+ 'sqlite3': ['sqlite3'],
27
+ 'redis-cli': ['redis-cli'],
28
+ 'mongo': ['mongo'],
29
+ 'ssh': ['ssh'],
30
+ 'telnet': ['telnet'],
31
+ 'ftp': ['ftp'],
32
+ 'sftp': ['sftp'],
33
+ 'top': ['top'],
34
+ 'htop': ['htop'],
35
+ 'watch': ['watch'],
36
+ 'r': ['R', '--interactive'],
37
+ }
38
+
39
+
40
+ def validate_bash_command(command_parts: List[str]) -> bool:
41
+ """
42
+ Check if the command is a valid bash command.
43
+
44
+ Returns True if the command exists in PATH or is a shell builtin.
45
+ """
46
+ if not command_parts:
47
+ return False
48
+
49
+ cmd = command_parts[0]
50
+
51
+ # Check shell builtins
52
+ builtins = {'cd', 'pwd', 'echo', 'export', 'source', 'alias', 'unalias',
53
+ 'history', 'set', 'unset', 'read', 'eval', 'exec', 'exit',
54
+ 'return', 'shift', 'trap', 'wait', 'jobs', 'fg', 'bg',
55
+ 'kill', 'ulimit', 'umask', 'type', 'hash', 'true', 'false'}
56
+
57
+ if cmd in builtins:
58
+ return True
59
+
60
+ # Check if command exists in PATH
61
+ return shutil.which(cmd) is not None
62
+
63
+
64
+ def handle_bash_command(
65
+ cmd_parts: List[str],
66
+ full_cmd: str,
67
+ stdin_input: Optional[str],
68
+ state: Any
69
+ ) -> Tuple[bool, str]:
70
+ """
71
+ Execute a bash command and return the result.
72
+
73
+ Args:
74
+ cmd_parts: Parsed command parts
75
+ full_cmd: Full command string
76
+ stdin_input: Input to pipe to command
77
+ state: Shell state
78
+
79
+ Returns:
80
+ Tuple of (success, output)
81
+ """
82
+ try:
83
+ result = subprocess.run(
84
+ full_cmd,
85
+ shell=True,
86
+ capture_output=True,
87
+ text=True,
88
+ cwd=state.current_path,
89
+ input=stdin_input,
90
+ timeout=300
91
+ )
92
+
93
+ if result.returncode == 0:
94
+ output = result.stdout
95
+ if result.stderr:
96
+ output += f"\n{result.stderr}"
97
+ return True, output.strip()
98
+ else:
99
+ error = result.stderr or result.stdout or f"Command exited with code {result.returncode}"
100
+ return False, error.strip()
101
+
102
+ except subprocess.TimeoutExpired:
103
+ return False, "Command timed out after 5 minutes"
104
+ except Exception as e:
105
+ return False, str(e)
106
+
107
+
108
+ def open_terminal_editor(command: str) -> str:
109
+ """Open a terminal editor command interactively"""
110
+ try:
111
+ subprocess.run(command, shell=True)
112
+ return "Editor session completed"
113
+ except Exception as e:
114
+ return f"Editor error: {e}"
115
+
116
+
117
+ def handle_cd_command(cmd_parts: List[str], state: Any) -> Tuple[Any, str]:
118
+ """Handle the cd command"""
119
+ if len(cmd_parts) < 2:
120
+ new_path = os.path.expanduser("~")
121
+ else:
122
+ new_path = os.path.expanduser(cmd_parts[1])
123
+
124
+ if not os.path.isabs(new_path):
125
+ new_path = os.path.join(state.current_path, new_path)
126
+
127
+ new_path = os.path.normpath(new_path)
128
+
129
+ if os.path.isdir(new_path):
130
+ state.current_path = new_path
131
+ os.chdir(new_path)
132
+ return state, f"Changed to: {new_path}"
133
+ else:
134
+ return state, colored(f"Directory not found: {new_path}", "red")
135
+
136
+
137
+ def handle_interactive_command(cmd_parts: List[str], state: Any) -> Tuple[Any, str]:
138
+ """Handle interactive commands by running them in a subprocess"""
139
+ command = ' '.join(cmd_parts)
140
+ try:
141
+ subprocess.run(command, shell=True, cwd=state.current_path)
142
+ return state, f"Interactive session ({cmd_parts[0]}) completed"
143
+ except KeyboardInterrupt:
144
+ return state, colored("Session interrupted", "yellow")
145
+ except Exception as e:
146
+ return state, colored(f"Error: {e}", "red")
147
+
148
+
149
+ def start_interactive_session(command: str) -> int:
150
+ """Start an interactive shell session"""
151
+ try:
152
+ return subprocess.call(command, shell=True)
153
+ except Exception as e:
154
+ print(colored(f"Error starting session: {e}", "red"))
155
+ return 1
156
+
157
+
158
+ def list_directory(args: List[str]) -> None:
159
+ """List directory contents with formatting"""
160
+ path = args[0] if args else "."
161
+ path = os.path.expanduser(path)
162
+
163
+ if not os.path.exists(path):
164
+ print(colored(f"Path not found: {path}", "red"))
165
+ return
166
+
167
+ if os.path.isfile(path):
168
+ print(path)
169
+ return
170
+
171
+ try:
172
+ entries = os.listdir(path)
173
+ entries.sort()
174
+
175
+ for entry in entries:
176
+ full_path = os.path.join(path, entry)
177
+ if os.path.isdir(full_path):
178
+ print(colored(entry + "/", "blue", attrs=["bold"]))
179
+ elif os.access(full_path, os.X_OK):
180
+ print(colored(entry, "green", attrs=["bold"]))
181
+ else:
182
+ print(entry)
183
+
184
+ except PermissionError:
185
+ print(colored(f"Permission denied: {path}", "red"))
npcsh/guac.py ADDED
@@ -0,0 +1,46 @@
1
+ """
2
+ guac - Python data analysis mode CLI entry point
3
+
4
+ This is a thin wrapper that executes the guac.jinx through the jinx mechanism.
5
+ """
6
+ import argparse
7
+ import os
8
+ import sys
9
+
10
+ from npcsh._state import setup_shell
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser(description="guac - Python data analysis mode")
15
+ parser.add_argument("--model", "-m", type=str, help="LLM model to use")
16
+ parser.add_argument("--provider", "-p", type=str, help="LLM provider to use")
17
+ parser.add_argument("--plots-dir", type=str, help="Directory to save plots")
18
+ args = parser.parse_args()
19
+
20
+ # Setup shell to get team and default NPC
21
+ command_history, team, default_npc = setup_shell()
22
+
23
+ if not team or "guac" not in team.jinxs_dict:
24
+ print("Error: guac jinx not found. Ensure npc_team/jinxs/modes/guac.jinx exists.")
25
+ sys.exit(1)
26
+
27
+ # Build context for jinx execution
28
+ context = {
29
+ "npc": default_npc,
30
+ "team": team,
31
+ "messages": [],
32
+ "model": args.model,
33
+ "provider": args.provider,
34
+ "plots_dir": args.plots_dir,
35
+ }
36
+
37
+ # Execute the jinx
38
+ guac_jinx = team.jinxs_dict["guac"]
39
+ result = guac_jinx.execute(context=context, npc=default_npc)
40
+
41
+ if isinstance(result, dict) and result.get("output"):
42
+ print(result["output"])
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()