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.
- npcsh/_state.py +3508 -0
- npcsh/alicanto.py +65 -0
- npcsh/build.py +291 -0
- npcsh/completion.py +206 -0
- npcsh/config.py +163 -0
- npcsh/corca.py +50 -0
- npcsh/execution.py +185 -0
- npcsh/guac.py +46 -0
- npcsh/mcp_helpers.py +357 -0
- npcsh/mcp_server.py +299 -0
- npcsh/npc.py +323 -0
- npcsh/npc_team/alicanto.npc +2 -0
- npcsh/npc_team/alicanto.png +0 -0
- npcsh/npc_team/corca.npc +12 -0
- npcsh/npc_team/corca.png +0 -0
- npcsh/npc_team/corca_example.png +0 -0
- npcsh/npc_team/foreman.npc +7 -0
- npcsh/npc_team/frederic.npc +6 -0
- npcsh/npc_team/frederic4.png +0 -0
- npcsh/npc_team/guac.png +0 -0
- npcsh/npc_team/jinxs/code/python.jinx +11 -0
- npcsh/npc_team/jinxs/code/sh.jinx +34 -0
- npcsh/npc_team/jinxs/code/sql.jinx +16 -0
- npcsh/npc_team/jinxs/modes/alicanto.jinx +194 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +249 -0
- npcsh/npc_team/jinxs/modes/guac.jinx +317 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +214 -0
- npcsh/npc_team/jinxs/modes/pti.jinx +170 -0
- npcsh/npc_team/jinxs/modes/spool.jinx +161 -0
- npcsh/npc_team/jinxs/modes/wander.jinx +186 -0
- npcsh/npc_team/jinxs/modes/yap.jinx +262 -0
- npcsh/npc_team/jinxs/npc_studio/npc-studio.jinx +77 -0
- npcsh/npc_team/jinxs/utils/agent.jinx +17 -0
- npcsh/npc_team/jinxs/utils/chat.jinx +44 -0
- npcsh/npc_team/jinxs/utils/cmd.jinx +44 -0
- npcsh/npc_team/jinxs/utils/compress.jinx +140 -0
- npcsh/npc_team/jinxs/utils/core/build.jinx +65 -0
- npcsh/npc_team/jinxs/utils/core/compile.jinx +50 -0
- npcsh/npc_team/jinxs/utils/core/help.jinx +52 -0
- npcsh/npc_team/jinxs/utils/core/init.jinx +41 -0
- npcsh/npc_team/jinxs/utils/core/jinxs.jinx +32 -0
- npcsh/npc_team/jinxs/utils/core/set.jinx +40 -0
- npcsh/npc_team/jinxs/utils/edit_file.jinx +94 -0
- npcsh/npc_team/jinxs/utils/load_file.jinx +35 -0
- npcsh/npc_team/jinxs/utils/ots.jinx +61 -0
- npcsh/npc_team/jinxs/utils/roll.jinx +68 -0
- npcsh/npc_team/jinxs/utils/sample.jinx +56 -0
- npcsh/npc_team/jinxs/utils/search.jinx +130 -0
- npcsh/npc_team/jinxs/utils/serve.jinx +26 -0
- npcsh/npc_team/jinxs/utils/sleep.jinx +116 -0
- npcsh/npc_team/jinxs/utils/trigger.jinx +61 -0
- npcsh/npc_team/jinxs/utils/usage.jinx +33 -0
- npcsh/npc_team/jinxs/utils/vixynt.jinx +144 -0
- npcsh/npc_team/kadiefa.npc +3 -0
- npcsh/npc_team/kadiefa.png +0 -0
- npcsh/npc_team/npcsh.ctx +18 -0
- npcsh/npc_team/npcsh_sibiji.png +0 -0
- npcsh/npc_team/plonk.npc +2 -0
- npcsh/npc_team/plonk.png +0 -0
- npcsh/npc_team/plonkjr.npc +2 -0
- npcsh/npc_team/plonkjr.png +0 -0
- npcsh/npc_team/sibiji.npc +3 -0
- npcsh/npc_team/sibiji.png +0 -0
- npcsh/npc_team/spool.png +0 -0
- npcsh/npc_team/yap.png +0 -0
- npcsh/npcsh.py +296 -112
- npcsh/parsing.py +118 -0
- npcsh/plonk.py +54 -0
- npcsh/pti.py +54 -0
- npcsh/routes.py +139 -0
- npcsh/spool.py +48 -0
- npcsh/ui.py +199 -0
- npcsh/wander.py +62 -0
- npcsh/yap.py +50 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/agent.jinx +17 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.jinx +194 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/build.jinx +65 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/chat.jinx +44 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/cmd.jinx +44 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/compile.jinx +50 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/compress.jinx +140 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.jinx +249 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.npc +12 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/corca_example.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/edit_file.jinx +94 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/foreman.npc +7 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/frederic.npc +6 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/frederic4.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/guac.jinx +317 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/guac.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/help.jinx +52 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/init.jinx +41 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/jinxs.jinx +32 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.npc +3 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/load_file.jinx +35 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npc-studio.jinx +77 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npcsh.ctx +18 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/ots.jinx +61 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.jinx +214 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.npc +2 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/pti.jinx +170 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/python.jinx +11 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/roll.jinx +68 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sample.jinx +56 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/search.jinx +130 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/serve.jinx +26 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/set.jinx +40 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sh.jinx +34 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.npc +3 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sleep.jinx +116 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/spool.jinx +161 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/spool.png +0 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/sql.jinx +16 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/trigger.jinx +61 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/usage.jinx +33 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/vixynt.jinx +144 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/wander.jinx +186 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/yap.jinx +262 -0
- npcsh-1.1.13.data/data/npcsh/npc_team/yap.png +0 -0
- npcsh-1.1.13.dist-info/METADATA +522 -0
- npcsh-1.1.13.dist-info/RECORD +135 -0
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info}/WHEEL +1 -1
- npcsh-1.1.13.dist-info/entry_points.txt +9 -0
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info/licenses}/LICENSE +1 -1
- npcsh/command_history.py +0 -81
- npcsh/helpers.py +0 -36
- npcsh/llm_funcs.py +0 -295
- npcsh/main.py +0 -5
- npcsh/modes.py +0 -343
- npcsh/npc_compiler.py +0 -124
- npcsh-0.1.2.dist-info/METADATA +0 -99
- npcsh-0.1.2.dist-info/RECORD +0 -14
- npcsh-0.1.2.dist-info/entry_points.txt +0 -2
- {npcsh-0.1.2.dist-info → npcsh-1.1.13.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
npcsh/npc_team/spool.png
ADDED
|
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
|
|
5
|
-
|
|
6
|
-
from
|
|
7
|
-
|
|
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
|
-
|
|
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 .
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
64
|
+
return f"${c:.2f}"
|
|
79
65
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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()
|