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
@@ -0,0 +1,2 @@
1
+ name: plonkjr
2
+ primary_directive: You are junior automation specialist in the NPC Team.
Binary file
@@ -0,0 +1,3 @@
1
+ name: sibiji
2
+ primary_directive: You are a foundational AI assistant. Your role is to provide support and information. Respond to queries concisely and accurately. Help users with code and other processes.
3
+ jinxs: "*"
Binary file
Binary file
npcsh/npc_team/yap.png ADDED
Binary file
npcsh/npcsh.py CHANGED
@@ -1,135 +1,319 @@
1
- # npcsh.py
2
1
  import os
2
+ import sys
3
+ import argparse
4
+ import importlib.metadata
3
5
 
4
- import readline
5
- import atexit
6
- from datetime import datetime
7
- import pandas as pd
6
+ import platform
7
+ try:
8
+ from termcolor import colored
9
+ except:
10
+ pass
11
+ from npcpy.npc_sysenv import (
12
+ render_markdown,
13
+ )
14
+ from npcpy.memory.command_history import (
15
+ CommandHistory,
16
+ load_kg_from_db,
17
+ save_kg_to_db,
18
+ )
19
+ from npcpy.npc_compiler import NPC
20
+ from npcpy.memory.knowledge_graph import (
21
+ kg_evolve_incremental
22
+ )
8
23
 
9
- # Configure logging
24
+ try:
25
+ import readline
26
+ except:
27
+ print('no readline support, some features may not work as desired. ')
10
28
 
29
+ try:
30
+ VERSION = importlib.metadata.version("npcsh")
31
+ except importlib.metadata.PackageNotFoundError:
32
+ VERSION = "unknown"
11
33
 
12
- from .command_history import CommandHistory
13
- from .llm_funcs import (
14
- get_llm_response,
15
- execute_llm_command,
16
- check_llm_command,
17
- execute_llm_question,
18
- execute_llm_thought,
34
+ from npcsh._state import (
35
+ initial_state,
36
+ orange,
37
+ ShellState,
38
+ execute_command,
39
+ make_completer,
40
+ process_result,
41
+ readline_safe_prompt,
42
+ setup_shell,
43
+ get_multiline_input,
19
44
  )
20
- from .modes import (
21
- enter_bash_mode,
22
- enter_whisper_mode,
23
- enter_notes_mode,
24
- enter_observation_mode,
25
- enter_spool_mode,
26
- )
27
- from .helpers import log_action, list_directory, read_file
28
- from .npc_compiler import NPCCompiler
29
-
30
-
31
- def execute_command(command, command_history, db_path, npc_compiler):
32
- subcommands = []
33
- output = ""
34
- location = os.getcwd()
35
-
36
- if command.startswith("/"):
37
- command = command[1:]
38
- log_action("Command Executed", command)
39
-
40
- command_parts = command.split()
41
- command_name = command_parts[0]
42
- args = command_parts[1:]
43
-
44
- if (
45
- command_name == "b"
46
- or command_name == "bash"
47
- or command_name == "zsh"
48
- or command_name == "sh"
49
- ):
50
- output = enter_bash_mode()
51
- if command_name == "compile" or command_name == "com":
52
- try:
53
- compiled_script = npc_compiler.compile(args[0])
54
- output = f"Compiled NPC profile: {compiled_script}"
55
- print(output)
56
- except Exception as e:
57
- output = f"Error compiling NPC profile: {str(e)}"
58
- print(output)
59
-
60
- elif command_name == "whisper":
61
- output = enter_whisper_mode(command_history)
62
- elif command_name == "notes":
63
- output = enter_notes_mode(command_history)
64
- elif command_name == "data":
65
- print(db_path)
66
- output = enter_observation_mode(command_history)
67
- elif command_name == "cmd" or command_name == "command":
68
- output = execute_llm_command(command, command_history)
69
- elif command_name == "?":
70
- output = execute_llm_question(command, command_history)
71
- elif command_name == "th" or command_name == "thought":
72
- output = execute_llm_thought(command, command_history)
73
- elif command_name == "spool" or command_name == "sp":
74
- inherit_last = int(args[0]) if args else 0
75
-
76
- output = enter_spool_mode(command_history, inherit_last)
45
+
46
+
47
+ def display_usage(state: ShellState):
48
+ """Display token usage and cost summary."""
49
+ inp = state.session_input_tokens
50
+ out = state.session_output_tokens
51
+ cost = state.session_cost_usd
52
+ turns = state.turn_count
53
+ total = inp + out
54
+
55
+ def fmt(n):
56
+ return f"{n/1000:.1f}k" if n >= 1000 else str(n)
57
+
58
+ def fmt_cost(c):
59
+ if c == 0:
60
+ return "free"
61
+ elif c < 0.01:
62
+ return f"${c:.4f}"
77
63
  else:
78
- output = f"Unknown command: {command_name}"
64
+ return f"${c:.2f}"
79
65
 
80
- subcommands = [f"/{command}"]
81
- else:
82
- output = check_llm_command(command, command_history)
66
+ print(colored("\n─────────────────────────────", "cyan"))
67
+ print(colored("📊 Session Usage", "cyan", attrs=["bold"]))
68
+ print(f" Tokens: {fmt(inp)} in / {fmt(out)} out ({fmt(total)} total)")
69
+ print(f" Cost: {fmt_cost(cost)}")
70
+ print(f" Turns: {turns}")
71
+ print(colored("─────────────────────────────\n", "cyan"))
83
72
 
84
- command_history.add(command, subcommands, output, location)
85
73
 
74
+ def print_welcome_message():
75
+ print(
76
+ """
77
+ ___________________________________________
78
+ ___________________________________________
79
+ ___________________________________________
86
80
 
87
- def setup_readline():
88
- readline.set_history_length(1000)
89
- readline.parse_and_bind("set editing-mode vi")
90
- readline.parse_and_bind('"\e[A": history-search-backward')
91
- readline.parse_and_bind('"\e[B": history-search-forward')
81
+ Welcome to \033[1;94mnpc\033[0m\033[1;38;5;202msh\033[0m!
82
+ \033[1;94m \033[0m\033[1;38;5;202m _ \\\\
83
+ \033[1;94m _ __ _ __ ___ \033[0m\033[1;38;5;202m ___ | |___ \\\\
84
+ \033[1;94m| '_ \\ | '_ \\ / __|\033[0m\033[1;38;5;202m / __/ | |_ _| \\\\
85
+ \033[1;94m| | | || |_) |( |__ \033[0m\033[1;38;5;202m \\_ \\ | | | | //
86
+ \033[1;94m|_| |_|| .__/ \\___|\033[0m\033[1;38;5;202m |___/ |_| |_| //
87
+ \033[1;94m|🤖| \033[0m\033[1;38;5;202m //
88
+ \033[1;94m|🤖|
89
+ \033[1;94m|🤖|
90
+ ___________________________________________
91
+ ___________________________________________
92
+ ___________________________________________
92
93
 
93
- readline.parse_and_bind('"\C-r": reverse-search-history')
94
+ Begin by asking a question, issuing a bash command, or typing '/help' for more information.
94
95
 
96
+ """
97
+ )
95
98
 
96
- def save_readline_history():
97
- readline.write_history_file(os.path.expanduser("~/.npcsh_readline_history"))
98
99
 
100
+ def run_repl(command_history: CommandHistory, initial_state: ShellState, router):
101
+ state = initial_state
102
+
103
+ print_welcome_message()
99
104
 
100
- def main():
101
- # check if theres a set path to a local db in the os env
102
- if "NPCSH_DB_PATH" in os.environ:
103
- db_path = os.environ["NPCSH_DB_PATH"]
104
- command_history = CommandHistory(db_path)
105
+ render_markdown(f'- Using {state.current_mode} mode. Use /agent, /cmd, or /chat to switch to other modes')
106
+ render_markdown(f'- To switch to a different NPC, type /npc <npc_name> or /n <npc_name> to switch to that NPC.')
107
+ render_markdown('\n- Here are the current NPCs available in your team: ' + ', '.join([npc_name for npc_name in state.team.npcs.keys()]))
108
+ render_markdown('\n- Here are the currently available Jinxs: ' + ', '.join([jinx_name for jinx_name in state.team.jinxs_dict.keys()]))
109
+
110
+ is_windows = platform.system().lower().startswith("win")
111
+ try:
112
+ completer = make_completer(state, router)
113
+ readline.set_completer(completer)
114
+ except:
115
+ pass
116
+ session_scopes = set()
117
+
118
+ def exit_shell(current_state: ShellState):
119
+ print("\nGoodbye!")
120
+ print(colored("Processing and archiving all session knowledge...", "cyan"))
121
+
122
+ engine = command_history.engine
123
+
124
+ for team_name, npc_name, path in session_scopes:
125
+ try:
126
+ print(f" -> Archiving knowledge for: T='{team_name}', N='{npc_name}', P='{path}'")
127
+
128
+ convo_id = current_state.conversation_id
129
+ all_messages = command_history.get_conversations_by_id(convo_id)
130
+
131
+ scope_messages = [
132
+ m for m in all_messages
133
+ if m.get('directory_path') == path and m.get('team') == team_name and m.get('npc') == npc_name
134
+ ]
135
+
136
+ full_text = "\n".join([f"{m['role']}: {m['content']}" for m in scope_messages if m.get('content')])
137
+
138
+ if not full_text.strip():
139
+ print(" ...No content for this scope, skipping.")
140
+ continue
141
+
142
+ current_kg = load_kg_from_db(engine, team_name, npc_name, path)
143
+
144
+ evolved_kg, _ = kg_evolve_incremental(
145
+ existing_kg=current_kg,
146
+ new_content_text=full_text,
147
+ model=current_state.npc.model,
148
+ provider=current_state.npc.provider,
149
+ npc= current_state.npc,
150
+ get_concepts=True,
151
+ link_concepts_facts = True,
152
+ link_concepts_concepts = True,
153
+ link_facts_facts = True,
154
+ )
155
+
156
+ save_kg_to_db(engine,
157
+ evolved_kg,
158
+ team_name,
159
+ npc_name,
160
+ path)
161
+
162
+ except Exception as e:
163
+ import traceback
164
+ print(colored(f"Failed to process KG for scope ({team_name}, {npc_name}, {path}): {e}", "red"))
165
+ traceback.print_exc()
166
+
167
+ sys.exit(0)
105
168
 
106
- else:
107
- db_path = "~/npcsh_history.db"
108
- command_history = CommandHistory()
109
- # Initialize NPCCompiler
110
- os.makedirs("./npc_profiles", exist_ok=True)
111
- npc_directory = os.path.expanduser(
112
- "./npc_profiles"
113
- ) # You can change this to your preferred directory
114
- npc_compiler = NPCCompiler(npc_directory)
115
-
116
- setup_readline()
117
- atexit.register(save_readline_history)
118
- atexit.register(command_history.close)
119
-
120
- print("Welcome to npcsh!")
121
169
  while True:
122
170
  try:
123
- user_input = input("npcsh> ").strip()
124
- if user_input.lower() in ["exit", "quit"]:
125
- print("Goodbye!")
126
- break
171
+ if state.messages is not None:
172
+ if len(state.messages) > 20:
173
+ # Display usage before compacting
174
+ display_usage(state)
175
+
176
+ planning_state = {
177
+ "goal": "ongoing npcsh session",
178
+ "facts": [f"Working in {state.current_path}", f"Current mode: {state.current_mode}"],
179
+ "successes": [],
180
+ "mistakes": [],
181
+ "todos": [],
182
+ "constraints": ["Follow user requests", "Use appropriate mode for tasks"]
183
+ }
184
+ compressed_state = state.npc.compress_planning_state(planning_state)
185
+ state.messages = [{"role": "system", "content": f"Session context: {compressed_state}"}]
186
+
187
+ try:
188
+ completer = make_completer(state, router)
189
+ readline.set_completer(completer)
190
+ except:
191
+ pass
192
+
193
+ display_model = state.chat_model
194
+ if isinstance(state.npc, NPC) and state.npc.model:
195
+ display_model = state.npc.model
196
+
197
+ npc_name = state.npc.name if isinstance(state.npc, NPC) else "npcsh"
198
+ team_name = state.team.name if state.team else ""
199
+
200
+ # Check if model is local (ollama) or remote (has cost)
201
+ provider = state.chat_provider
202
+ if isinstance(state.npc, NPC) and state.npc.provider:
203
+ provider = state.npc.provider
204
+ is_local = provider and provider.lower() in ['ollama', 'transformers', 'local']
205
+
206
+ # Build token/cost string for hint line
207
+ if state.session_input_tokens > 0 or state.session_output_tokens > 0:
208
+ usage_str = f"📊 {state.session_input_tokens:,} in / {state.session_output_tokens:,} out"
209
+ if not is_local and state.session_cost_usd > 0:
210
+ usage_str += f" | ${state.session_cost_usd:.4f}"
211
+ token_hint = colored(usage_str, "white", attrs=["dark"])
127
212
  else:
128
- execute_command(user_input, command_history, db_path, npc_compiler)
129
- except (KeyboardInterrupt, EOFError):
130
- print("\nGoodbye!")
131
- break
213
+ token_hint = ""
214
+
215
+ if is_windows:
216
+ print(f"cwd: {state.current_path}")
217
+ status = f"{npc_name}"
218
+ if team_name:
219
+ status += f" | {team_name}"
220
+ status += f" | {display_model}"
221
+ print(status)
222
+ prompt = "> "
223
+ else:
224
+ # Line 1: cwd (full path)
225
+ cwd_line = colored("📁 ", "blue") + colored(state.current_path, "blue")
226
+ print(cwd_line)
227
+
228
+ # Line 2: npc | team | model
229
+ npc_colored = orange(npc_name) if isinstance(state.npc, NPC) else colored("npcsh", "cyan")
230
+ parts = [colored("🤖 ", "yellow") + npc_colored]
231
+ if team_name:
232
+ parts.append(colored("👥 ", "magenta") + colored(team_name, "magenta"))
233
+ parts.append(colored(display_model, "white", attrs=["dark"]))
234
+ print(" | ".join(parts))
132
235
 
236
+ prompt = colored("> ", "green")
133
237
 
238
+ user_input = get_multiline_input(prompt, state=state, router=router, token_hint=token_hint).strip()
239
+
240
+ if user_input == "\x1a":
241
+ exit_shell(state)
242
+
243
+ if not user_input:
244
+ continue
245
+
246
+ if user_input.lower() in ["exit", "quit"]:
247
+ if isinstance(state.npc, NPC):
248
+ print(f"Exiting {state.npc.name} mode.")
249
+ state.npc = None
250
+ continue
251
+ else:
252
+ exit_shell(state)
253
+
254
+ team_name = state.team.name if state.team else "__none__"
255
+ npc_name = state.npc.name if isinstance(state.npc, NPC) else "__none__"
256
+ session_scopes.add((team_name, npc_name, state.current_path))
257
+
258
+ state, output = execute_command(user_input,
259
+ state,
260
+ review = False,
261
+ router=router,
262
+ command_history=command_history)
263
+
264
+ process_result(user_input,
265
+ state,
266
+ output,
267
+ command_history,
268
+ )
269
+
270
+ except KeyboardInterrupt:
271
+ print("^C")
272
+ if input("\nExit? (y/n): ").lower().startswith('y'):
273
+ exit_shell(state)
274
+ continue
275
+
276
+ except EOFError:
277
+ exit_shell(state)
278
+ except Exception as e:
279
+ if is_windows and "EOF" in str(e).lower():
280
+ print("\nHint: On Windows, use Ctrl+Z then Enter for EOF, or type 'exit'")
281
+ continue
282
+ raise
283
+
284
+
285
+ def main() -> None:
286
+ from npcsh.routes import router
287
+
288
+ parser = argparse.ArgumentParser(description="npcsh - An NPC-powered shell.")
289
+ parser.add_argument(
290
+ "-v", "--version", action="version", version=f"npcsh version {VERSION}"
291
+ )
292
+ parser.add_argument(
293
+ "-c", "--command", type=str, help="Execute a single command and exit."
294
+ )
295
+ args = parser.parse_args()
296
+
297
+ command_history, team, default_npc = setup_shell()
298
+
299
+ if team and hasattr(team, 'jinxs_dict'):
300
+ for jinx_name, jinx_obj in team.jinxs_dict.items():
301
+ router.register_jinx(jinx_obj)
302
+
303
+ initial_state.npc = default_npc
304
+ initial_state.team = team
305
+ if args.command:
306
+ state = initial_state
307
+ state.current_path = os.getcwd()
308
+ final_state, output = execute_command(args.command, state, router=router, command_history=command_history)
309
+ if final_state.stream_output:
310
+ for chunk in output:
311
+ print(str(chunk), end='')
312
+ print()
313
+ elif output is not None:
314
+ print(output)
315
+ else:
316
+ run_repl(command_history, initial_state, router)
317
+
134
318
  if __name__ == "__main__":
135
- main()
319
+ main()
npcsh/parsing.py ADDED
@@ -0,0 +1,118 @@
1
+ """
2
+ Command parsing utilities for npcsh
3
+ """
4
+ import shlex
5
+ from typing import List
6
+
7
+
8
+ def split_by_pipes(command: str) -> List[str]:
9
+ """
10
+ Split a command by pipes, preserving quoted strings.
11
+
12
+ Examples:
13
+ 'foo | bar' -> ['foo', 'bar']
14
+ 'foo "hello|world" | bar' -> ['foo "hello|world"', 'bar']
15
+ """
16
+ result = []
17
+ current = []
18
+ in_single_quote = False
19
+ in_double_quote = False
20
+ i = 0
21
+
22
+ while i < len(command):
23
+ char = command[i]
24
+
25
+ if char == "'" and not in_double_quote:
26
+ in_single_quote = not in_single_quote
27
+ current.append(char)
28
+ elif char == '"' and not in_single_quote:
29
+ in_double_quote = not in_double_quote
30
+ current.append(char)
31
+ elif char == '|' and not in_single_quote and not in_double_quote:
32
+ result.append(''.join(current).strip())
33
+ current = []
34
+ else:
35
+ current.append(char)
36
+
37
+ i += 1
38
+
39
+ # Add final segment
40
+ if current:
41
+ result.append(''.join(current).strip())
42
+
43
+ return [s for s in result if s]
44
+
45
+
46
+ def parse_command_safely(cmd: str) -> List[str]:
47
+ """
48
+ Safely parse a command string into parts using shlex.
49
+
50
+ Returns an empty list on parse errors.
51
+ """
52
+ try:
53
+ return shlex.split(cmd)
54
+ except ValueError:
55
+ # Handle unmatched quotes, etc
56
+ return cmd.split()
57
+
58
+
59
+ def parse_generic_command_flags(parts: List[str]) -> tuple:
60
+ """
61
+ Parse command flags in a generic way.
62
+
63
+ Returns:
64
+ Tuple of (parsed_flags dict, remaining_parts list)
65
+ """
66
+ parsed_flags = {}
67
+ remaining = []
68
+
69
+ i = 0
70
+ while i < len(parts):
71
+ part = parts[i]
72
+
73
+ if part.startswith('--'):
74
+ key = part[2:]
75
+ if '=' in key:
76
+ key, value = key.split('=', 1)
77
+ parsed_flags[key] = _try_convert_type(value)
78
+ elif i + 1 < len(parts) and not parts[i + 1].startswith('-'):
79
+ parsed_flags[key] = _try_convert_type(parts[i + 1])
80
+ i += 1
81
+ else:
82
+ parsed_flags[key] = True
83
+ elif part.startswith('-') and len(part) == 2:
84
+ key = part[1]
85
+ if i + 1 < len(parts) and not parts[i + 1].startswith('-'):
86
+ parsed_flags[key] = _try_convert_type(parts[i + 1])
87
+ i += 1
88
+ else:
89
+ parsed_flags[key] = True
90
+ else:
91
+ remaining.append(part)
92
+
93
+ i += 1
94
+
95
+ return parsed_flags, remaining
96
+
97
+
98
+ def _try_convert_type(value: str):
99
+ """Try to convert string value to appropriate Python type"""
100
+ # Try int
101
+ try:
102
+ return int(value)
103
+ except ValueError:
104
+ pass
105
+
106
+ # Try float
107
+ try:
108
+ return float(value)
109
+ except ValueError:
110
+ pass
111
+
112
+ # Try bool
113
+ if value.lower() in ('true', 'yes', '1'):
114
+ return True
115
+ if value.lower() in ('false', 'no', '0'):
116
+ return False
117
+
118
+ return value
npcsh/plonk.py ADDED
@@ -0,0 +1,54 @@
1
+ """
2
+ plonk - Vision-based GUI automation CLI entry point
3
+
4
+ This is a thin wrapper that executes the plonk.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="plonk - Vision-based GUI automation")
15
+ parser.add_argument("task", nargs="*", help="Task description for GUI automation")
16
+ parser.add_argument("--vmodel", type=str, help="Vision model to use (default: gpt-4o)")
17
+ parser.add_argument("--vprovider", type=str, help="Vision provider (default: openai)")
18
+ parser.add_argument("--max-iterations", type=int, default=10, help="Maximum iterations")
19
+ parser.add_argument("--no-debug", action="store_true", help="Disable debug output")
20
+ args = parser.parse_args()
21
+
22
+ if not args.task:
23
+ parser.print_help()
24
+ sys.exit(1)
25
+
26
+ # Setup shell to get team and default NPC
27
+ command_history, team, default_npc = setup_shell()
28
+
29
+ if not team or "plonk" not in team.jinxs_dict:
30
+ print("Error: plonk jinx not found. Ensure npc_team/jinxs/modes/plonk.jinx exists.")
31
+ sys.exit(1)
32
+
33
+ # Build context for jinx execution
34
+ context = {
35
+ "npc": default_npc,
36
+ "team": team,
37
+ "messages": [],
38
+ "task": " ".join(args.task),
39
+ "vmodel": args.vmodel,
40
+ "vprovider": args.vprovider,
41
+ "max_iterations": args.max_iterations,
42
+ "debug": not args.no_debug,
43
+ }
44
+
45
+ # Execute the jinx
46
+ plonk_jinx = team.jinxs_dict["plonk"]
47
+ result = plonk_jinx.execute(context=context, npc=default_npc)
48
+
49
+ if isinstance(result, dict) and result.get("output"):
50
+ print(result["output"])
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
npcsh/pti.py ADDED
@@ -0,0 +1,54 @@
1
+ """
2
+ pti - Pardon-The-Interruption mode CLI entry point
3
+
4
+ This is a thin wrapper that executes the pti.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="pti - Human-in-the-loop reasoning mode")
15
+ parser.add_argument("prompt", nargs="*", help="Initial prompt to start with")
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("--files", "-f", nargs="*", help="Files to load into context")
19
+ parser.add_argument("--reasoning-model", type=str, help="Model for reasoning (may differ from chat)")
20
+ args = parser.parse_args()
21
+
22
+ # Setup shell to get team and default NPC
23
+ command_history, team, default_npc = setup_shell()
24
+
25
+ if not team or "pti" not in team.jinxs_dict:
26
+ print("Error: pti jinx not found. Ensure npc_team/jinxs/modes/pti.jinx exists.")
27
+ sys.exit(1)
28
+
29
+ # Build context for jinx execution
30
+ context = {
31
+ "npc": default_npc,
32
+ "team": team,
33
+ "messages": [],
34
+ "model": args.model,
35
+ "provider": args.provider,
36
+ "files": ",".join(args.files) if args.files else None,
37
+ "reasoning_model": args.reasoning_model,
38
+ }
39
+
40
+ # If initial prompt provided, add it
41
+ if args.prompt:
42
+ initial = " ".join(args.prompt)
43
+ context["messages"] = [{"role": "user", "content": initial}]
44
+
45
+ # Execute the jinx
46
+ pti_jinx = team.jinxs_dict["pti"]
47
+ result = pti_jinx.execute(context=context, npc=default_npc)
48
+
49
+ if isinstance(result, dict) and result.get("output"):
50
+ print(result["output"])
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()