npcsh 1.1.12__py3-none-any.whl → 1.1.14__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 (99) hide show
  1. npcsh/_state.py +700 -377
  2. npcsh/alicanto.py +54 -1153
  3. npcsh/completion.py +206 -0
  4. npcsh/config.py +163 -0
  5. npcsh/corca.py +35 -1462
  6. npcsh/execution.py +185 -0
  7. npcsh/guac.py +31 -1986
  8. npcsh/npc_team/jinxs/code/sh.jinx +11 -15
  9. npcsh/npc_team/jinxs/modes/alicanto.jinx +186 -80
  10. npcsh/npc_team/jinxs/modes/corca.jinx +243 -22
  11. npcsh/npc_team/jinxs/modes/guac.jinx +313 -42
  12. npcsh/npc_team/jinxs/modes/plonk.jinx +209 -48
  13. npcsh/npc_team/jinxs/modes/pti.jinx +167 -25
  14. npcsh/npc_team/jinxs/modes/spool.jinx +158 -37
  15. npcsh/npc_team/jinxs/modes/wander.jinx +179 -74
  16. npcsh/npc_team/jinxs/modes/yap.jinx +258 -21
  17. npcsh/npc_team/jinxs/utils/chat.jinx +39 -12
  18. npcsh/npc_team/jinxs/utils/cmd.jinx +44 -0
  19. npcsh/npc_team/jinxs/utils/search.jinx +3 -3
  20. npcsh/npc_team/jinxs/utils/usage.jinx +33 -0
  21. npcsh/npcsh.py +76 -20
  22. npcsh/parsing.py +118 -0
  23. npcsh/plonk.py +41 -329
  24. npcsh/pti.py +41 -201
  25. npcsh/spool.py +34 -239
  26. npcsh/ui.py +199 -0
  27. npcsh/wander.py +54 -542
  28. npcsh/yap.py +38 -570
  29. npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.jinx +194 -0
  30. npcsh-1.1.14.data/data/npcsh/npc_team/chat.jinx +44 -0
  31. npcsh-1.1.14.data/data/npcsh/npc_team/cmd.jinx +44 -0
  32. npcsh-1.1.14.data/data/npcsh/npc_team/corca.jinx +249 -0
  33. npcsh-1.1.14.data/data/npcsh/npc_team/guac.jinx +317 -0
  34. npcsh-1.1.14.data/data/npcsh/npc_team/plonk.jinx +214 -0
  35. npcsh-1.1.14.data/data/npcsh/npc_team/pti.jinx +170 -0
  36. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/search.jinx +3 -3
  37. npcsh-1.1.14.data/data/npcsh/npc_team/sh.jinx +34 -0
  38. npcsh-1.1.14.data/data/npcsh/npc_team/spool.jinx +161 -0
  39. npcsh-1.1.14.data/data/npcsh/npc_team/usage.jinx +33 -0
  40. npcsh-1.1.14.data/data/npcsh/npc_team/wander.jinx +186 -0
  41. npcsh-1.1.14.data/data/npcsh/npc_team/yap.jinx +262 -0
  42. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/METADATA +1 -1
  43. npcsh-1.1.14.dist-info/RECORD +135 -0
  44. npcsh-1.1.12.data/data/npcsh/npc_team/alicanto.jinx +0 -88
  45. npcsh-1.1.12.data/data/npcsh/npc_team/chat.jinx +0 -17
  46. npcsh-1.1.12.data/data/npcsh/npc_team/corca.jinx +0 -28
  47. npcsh-1.1.12.data/data/npcsh/npc_team/guac.jinx +0 -46
  48. npcsh-1.1.12.data/data/npcsh/npc_team/plonk.jinx +0 -53
  49. npcsh-1.1.12.data/data/npcsh/npc_team/pti.jinx +0 -28
  50. npcsh-1.1.12.data/data/npcsh/npc_team/sh.jinx +0 -38
  51. npcsh-1.1.12.data/data/npcsh/npc_team/spool.jinx +0 -40
  52. npcsh-1.1.12.data/data/npcsh/npc_team/wander.jinx +0 -81
  53. npcsh-1.1.12.data/data/npcsh/npc_team/yap.jinx +0 -25
  54. npcsh-1.1.12.dist-info/RECORD +0 -126
  55. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/agent.jinx +0 -0
  56. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  57. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/alicanto.png +0 -0
  58. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/build.jinx +0 -0
  59. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/compile.jinx +0 -0
  60. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/compress.jinx +0 -0
  61. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca.npc +0 -0
  62. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca.png +0 -0
  63. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/corca_example.png +0 -0
  64. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  65. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/foreman.npc +0 -0
  66. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/frederic.npc +0 -0
  67. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/frederic4.png +0 -0
  68. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/guac.png +0 -0
  69. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/help.jinx +0 -0
  70. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/init.jinx +0 -0
  71. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
  72. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  73. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  74. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  75. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
  76. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  77. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  78. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/ots.jinx +0 -0
  79. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonk.npc +0 -0
  80. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonk.png +0 -0
  81. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  82. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  83. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/python.jinx +0 -0
  84. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/roll.jinx +0 -0
  85. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sample.jinx +0 -0
  86. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/serve.jinx +0 -0
  87. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/set.jinx +0 -0
  88. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  89. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sibiji.png +0 -0
  90. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  91. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/spool.png +0 -0
  92. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/sql.jinx +0 -0
  93. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  94. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
  95. {npcsh-1.1.12.data → npcsh-1.1.14.data}/data/npcsh/npc_team/yap.png +0 -0
  96. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/WHEEL +0 -0
  97. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/entry_points.txt +0 -0
  98. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/licenses/LICENSE +0 -0
  99. {npcsh-1.1.12.dist-info → npcsh-1.1.14.dist-info}/top_level.txt +0 -0
npcsh/completion.py ADDED
@@ -0,0 +1,206 @@
1
+ """
2
+ Readline and tab completion for npcsh
3
+ """
4
+ import os
5
+ import shutil
6
+ from typing import List, Any, Optional
7
+
8
+ try:
9
+ import readline
10
+ except ImportError:
11
+ readline = None
12
+
13
+ from .config import READLINE_HISTORY_FILE
14
+
15
+
16
+ def setup_readline() -> str:
17
+ """Set up readline with history and completion"""
18
+ if readline is None:
19
+ return ""
20
+
21
+ history_file = READLINE_HISTORY_FILE
22
+
23
+ try:
24
+ readline.read_history_file(history_file)
25
+ except FileNotFoundError:
26
+ pass
27
+
28
+ readline.set_history_length(10000)
29
+ readline.parse_and_bind("tab: complete")
30
+
31
+ return history_file
32
+
33
+
34
+ def save_readline_history():
35
+ """Save readline history to file"""
36
+ if readline is None:
37
+ return
38
+
39
+ try:
40
+ readline.write_history_file(READLINE_HISTORY_FILE)
41
+ except Exception:
42
+ pass
43
+
44
+
45
+ def get_path_executables() -> List[str]:
46
+ """Get list of executables in PATH"""
47
+ executables = set()
48
+
49
+ path_dirs = os.environ.get("PATH", "").split(os.pathsep)
50
+
51
+ for path_dir in path_dirs:
52
+ if os.path.isdir(path_dir):
53
+ try:
54
+ for entry in os.listdir(path_dir):
55
+ full_path = os.path.join(path_dir, entry)
56
+ if os.access(full_path, os.X_OK):
57
+ executables.add(entry)
58
+ except PermissionError:
59
+ pass
60
+
61
+ return sorted(executables)
62
+
63
+
64
+ def get_file_completions(text: str) -> List[str]:
65
+ """Get file/directory completions for text"""
66
+ completions = []
67
+
68
+ if text.startswith("~"):
69
+ expanded = os.path.expanduser(text)
70
+ prefix = "~"
71
+ search_path = expanded
72
+ else:
73
+ prefix = ""
74
+ search_path = text
75
+
76
+ # Get directory to search
77
+ if os.path.isdir(search_path):
78
+ dir_path = search_path
79
+ name_prefix = ""
80
+ else:
81
+ dir_path = os.path.dirname(search_path) or "."
82
+ name_prefix = os.path.basename(search_path)
83
+
84
+ if not os.path.isdir(dir_path):
85
+ return completions
86
+
87
+ try:
88
+ for entry in os.listdir(dir_path):
89
+ if entry.startswith(name_prefix):
90
+ full_path = os.path.join(dir_path, entry)
91
+ if os.path.isdir(full_path):
92
+ completions.append(entry + "/")
93
+ else:
94
+ completions.append(entry)
95
+ except PermissionError:
96
+ pass
97
+
98
+ return completions
99
+
100
+
101
+ def get_slash_commands(state: Any, router: Any) -> List[str]:
102
+ """Get list of available slash commands"""
103
+ commands = set()
104
+
105
+ # Built-in commands and modes
106
+ commands.update([
107
+ '/help', '/set', '/agent', '/chat', '/cmd',
108
+ '/sq', '/quit', '/exit', '/clear',
109
+ ])
110
+
111
+ # Team jinxs
112
+ if state.team and hasattr(state.team, 'jinxs_dict'):
113
+ for name in state.team.jinxs_dict:
114
+ commands.add(f'/{name}')
115
+
116
+ # Router jinxs
117
+ if router and hasattr(router, 'jinx_routes'):
118
+ for name in router.jinx_routes:
119
+ commands.add(f'/{name}')
120
+
121
+ return sorted(commands)
122
+
123
+
124
+ def get_npc_mentions(state: Any) -> List[str]:
125
+ """Get list of available @npc mentions"""
126
+ npcs = set()
127
+
128
+ # Team NPCs
129
+ if state.team and hasattr(state.team, 'npcs'):
130
+ for name in state.team.npcs:
131
+ npcs.add(f'@{name}')
132
+
133
+ # Also add forenpc if available
134
+ if state.team and hasattr(state.team, 'forenpc') and state.team.forenpc:
135
+ npcs.add(f'@{state.team.forenpc.name}')
136
+
137
+ # Default NPCs if team not loaded yet
138
+ if not npcs:
139
+ npcs.update(['@sibiji', '@guac', '@corca', '@kadiefa', '@plonk', '@forenpc'])
140
+
141
+ return sorted(npcs)
142
+
143
+
144
+ def is_command_position(buffer: str, begidx: int) -> bool:
145
+ """Check if we're completing a command (vs argument)"""
146
+ # If we're at the start or after a pipe, it's command position
147
+ before = buffer[:begidx].strip()
148
+ return not before or before.endswith('|')
149
+
150
+
151
+ def make_completer(shell_state: Any, router: Any):
152
+ """Create a completer function for readline"""
153
+
154
+ executables = get_path_executables()
155
+
156
+ def completer(text: str, state: int):
157
+ if readline is None:
158
+ return None
159
+
160
+ try:
161
+ buffer = readline.get_line_buffer()
162
+ begidx = readline.get_begidx()
163
+
164
+ # Build completion options
165
+ options = []
166
+
167
+ # Refresh slash commands and NPC mentions each time (they may change)
168
+ slash_commands = get_slash_commands(shell_state, router)
169
+ npc_mentions = get_npc_mentions(shell_state)
170
+
171
+ if text.startswith('/'):
172
+ # Slash command completion
173
+ options = [c for c in slash_commands if c.startswith(text)]
174
+
175
+ elif text.startswith('@'):
176
+ # @npc mention completion
177
+ options = [n for n in npc_mentions if n.startswith(text)]
178
+
179
+ elif text.startswith('~') or '/' in text or text.startswith('.'):
180
+ # File path completion
181
+ options = get_file_completions(text)
182
+
183
+ elif is_command_position(buffer, begidx):
184
+ # Command completion
185
+ options = [e for e in executables if e.startswith(text)]
186
+
187
+ else:
188
+ # Default to file completion
189
+ options = get_file_completions(text)
190
+
191
+ if state < len(options):
192
+ return options[state]
193
+ return None
194
+
195
+ except Exception:
196
+ return None
197
+
198
+ return completer
199
+
200
+
201
+ def readline_safe_prompt(prompt: str) -> str:
202
+ """Make prompt safe for readline (escape ANSI codes)"""
203
+ if readline is None:
204
+ return prompt
205
+ # Wrap non-printing characters
206
+ return prompt.replace('\x1b[', '\x01\x1b[').replace('m', 'm\x02')
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