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/pti.py CHANGED
@@ -1,214 +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
1
7
  import os
2
8
  import sys
3
- import shlex
4
- import argparse
5
- from typing import Dict, List, Any, Optional
6
-
7
- from termcolor import colored
8
-
9
- from npcpy.memory.command_history import CommandHistory, save_conversation_message
10
- from npcpy.npc_sysenv import (
11
- render_markdown
12
- )
13
- from npcpy.llm_funcs import get_llm_response
14
- from npcpy.npc_compiler import NPC
15
- from npcpy.data.load import load_file_contents
16
-
17
- from npcsh._state import (
18
- ShellState,
19
- setup_shell,
20
- get_multiline_input,
21
- readline_safe_prompt,
22
- get_npc_path
23
- )
24
-
25
- ice = "🧊"
26
- bear = "🐻‍❄️"
27
- def print_pti_welcome_message():
28
-
29
- print(f"""
30
- Welcome to PTI Mode!
31
-
32
- {ice}{ice}{ice} {ice}{ice}{ice} {bear}
33
- {ice} {ice} {ice} {bear}
34
- {ice}{ice}{ice} {ice} {bear}
35
- {ice} {ice} {bear}
36
- {ice} {ice} {bear}
37
-
38
- Pardon-The-Interruption for human-in-the-loop reasoning.
39
- Type 'exit' or 'quit' to return to the main shell.
40
- """)
41
-
42
- def enter_pti_mode(command: str, **kwargs):
43
- state: ShellState = kwargs.get('shell_state')
44
- command_history: CommandHistory = kwargs.get('command_history')
45
-
46
- if not state or not command_history:
47
- return {"output": "Error: PTI mode requires shell state and history.", "messages": kwargs.get('messages', [])}
48
-
49
- all_command_parts = shlex.split(command)
50
- parsed_args_list = all_command_parts[1:]
51
-
52
- parser = argparse.ArgumentParser(prog="/pti", description="Enter PTI mode for human-in-the-loop reasoning.")
53
- parser.add_argument('initial_prompt', nargs='*', help="Initial prompt to start the session.")
54
- parser.add_argument("-f", "--files", nargs="*", default=[], help="Files to load into context.")
55
-
56
- try:
57
- args = parser.parse_args(parsed_args_list)
58
- except SystemExit:
59
- return {"output": "Invalid arguments for /pti. Usage: /pti [initial prompt] [-f file1 file2 ...]", "messages": state.messages}
60
-
61
- print_pti_welcome_message()
62
-
63
- frederic_path = get_npc_path("frederic", command_history.db_path)
64
- state.npc = NPC(file=frederic_path)
65
- print(colored("Defaulting to NPC: frederic", "cyan"))
66
- state.npc = NPC(name="frederic")
67
9
 
68
- pti_messages = list(state.messages)
69
- loaded_content = {}
70
-
71
- if args.files:
72
- for file_path in args.files:
73
- try:
74
- content_chunks = load_file_contents(file_path)
75
- loaded_content[file_path] = "\n".join(content_chunks)
76
- print(colored(f"Successfully loaded content from: {file_path}", "green"))
77
- except Exception as e:
78
- print(colored(f"Error loading {file_path}: {e}", "red"))
79
-
80
- user_input = " ".join(args.initial_prompt)
10
+ from npcsh._state import setup_shell
81
11
 
82
- while True:
83
- try:
84
- if not user_input:
85
- npc_name = state.npc.name if state.npc and isinstance(state.npc, NPC) else "frederic"
86
- model_name = state.reasoning_model
87
-
88
- prompt_str = f"{colored(os.path.basename(state.current_path), 'blue')}:{npc_name}:{model_name}{bear}> "
89
- prompt = readline_safe_prompt(prompt_str)
90
- user_input = get_multiline_input(prompt).strip()
91
-
92
- if user_input.lower() in ["exit", "quit", "done"]:
93
- break
94
-
95
- if not user_input:
96
- continue
97
-
98
- prompt_for_llm = user_input
99
- if loaded_content:
100
- context_str = "\n".join([f"--- Content from {fname} ---\n{content}" for fname, content in loaded_content.items()])
101
- prompt_for_llm += f"\n\nUse the following context to inform your answer:\n{context_str}"
102
-
103
- prompt_for_llm += "\n\nThink step-by-step using <think> tags. When you need more information from me, enclose your question in <request_for_input> tags."
104
-
105
- save_conversation_message(
106
- command_history,
107
- state.conversation_id,
108
- "user",
109
- user_input,
110
- wd=state.current_path,
111
- model=state.reasoning_model,
112
- provider=state.reasoning_provider,
113
- npc=state.npc.name if isinstance(state.npc, NPC) else None,
114
- )
115
- pti_messages.append({"role": "user", "content": user_input})
116
-
117
- try:
118
- response_dict = get_llm_response(
119
- prompt=prompt_for_llm,
120
- model=state.reasoning_model,
121
- provider=state.reasoning_provider,
122
- messages=pti_messages,
123
- stream=True,
124
- npc=state.npc
125
- )
126
- stream = response_dict.get('response')
127
-
128
- response_chunks = []
129
- request_found = False
130
-
131
- for chunk in stream:
132
- chunk_content = ""
133
- if state.reasoning_provider == "ollama":
134
- chunk_content = chunk.get("message", {}).get("content", "")
135
- else:
136
- chunk_content = "".join(
137
- choice.delta.content
138
- for choice in chunk.choices
139
- if choice.delta.content is not None
140
- )
141
-
142
- print(chunk_content, end='')
143
- sys.stdout.flush()
144
- response_chunks.append(chunk_content)
145
-
146
- combined_text = "".join(response_chunks)
147
- if "</request_for_input>" in combined_text:
148
- request_found = True
149
- break
150
-
151
- full_response_text = "".join(response_chunks)
152
-
153
- save_conversation_message(
154
- command_history,
155
- state.conversation_id,
156
- "assistant",
157
- full_response_text,
158
- wd=state.current_path,
159
- model=state.reasoning_model,
160
- provider=state.reasoning_provider,
161
- npc=state.npc.name if isinstance(state.npc, NPC) else None,
162
- )
163
- pti_messages.append({"role": "assistant", "content": full_response_text})
164
-
165
- print()
166
- user_input = None
167
- continue
168
-
169
- except KeyboardInterrupt:
170
- print(colored("\n\n--- Stream Interrupted ---", "yellow"))
171
- interrupt_text = input('🐻‍❄️> ').strip()
172
- if interrupt_text:
173
- user_input = interrupt_text
174
- else:
175
- user_input = None
176
- continue
177
-
178
- except KeyboardInterrupt:
179
- print()
180
- continue
181
- except EOFError:
182
- print("\nExiting PTI Mode.")
183
- break
184
-
185
- render_markdown("\n# Exiting PTI Mode")
186
- return {"output": "", "messages": pti_messages}
187
12
 
188
13
  def main():
189
- parser = argparse.ArgumentParser(description="PTI - Pardon-The-Interruption human-in-the-loop shell.")
190
- parser.add_argument('initial_prompt', nargs='*', help="Initial prompt to start the session.")
191
- parser.add_argument("-f", "--files", nargs="*", default=[], help="Files to load into context.")
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)")
192
20
  args = parser.parse_args()
193
21
 
22
+ # Setup shell to get team and default NPC
194
23
  command_history, team, default_npc = setup_shell()
195
24
 
196
- from npcsh._state import initial_state
197
- initial_shell_state = initial_state
198
- initial_shell_state.team = team
199
- initial_shell_state.npc = default_npc
200
-
201
- fake_command_str = "/pti " + " ".join(args.initial_prompt)
202
- if args.files:
203
- fake_command_str += " --files " + " ".join(args.files)
204
-
205
- kwargs = {
206
- 'command': fake_command_str,
207
- 'shell_state': initial_shell_state,
208
- 'command_history': command_history
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,
209
38
  }
210
-
211
- enter_pti_mode(**kwargs)
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
+
212
52
 
213
53
  if __name__ == "__main__":
214
- main()
54
+ main()
npcsh/spool.py CHANGED
@@ -1,253 +1,48 @@
1
- from npcpy.memory.command_history import CommandHistory, start_new_conversation, save_conversation_message
2
- from npcpy.data.load import load_file_contents
3
- from npcpy.data.image import capture_screenshot
4
- from npcpy.data.text import rag_search
1
+ """
2
+ spool - Interactive chat mode CLI entry point
5
3
 
4
+ This is a thin wrapper that executes the spool.jinx through the jinx mechanism.
5
+ """
6
+ import argparse
6
7
  import os
7
8
  import sys
8
- from npcpy.npc_sysenv import (
9
- print_and_process_stream_with_markdown,
10
- get_system_message,
11
- render_markdown,
12
- )
13
- from npcsh._state import (
14
- orange,
15
- ShellState,
16
- execute_command,
17
- get_multiline_input,
18
- readline_safe_prompt,
19
- setup_shell,
20
- get_npc_path,
21
- process_result,
22
- initial_state,
23
- )
24
- from npcpy.llm_funcs import get_llm_response
25
- from npcpy.npc_compiler import NPC
26
- from typing import Any, List, Dict, Union
27
- from npcsh.yap import enter_yap_mode
28
- from termcolor import colored
29
- def print_spool_ascii():
30
- spool_art = """
31
- ██████╗██████╗ ████████╗ ████████╗ ██╗
32
- ██╔════╝██╔══██╗██╔🧵🧵🧵██ ██╔🧵🧵🧵██ ██║
33
- ╚█████╗ ██████╔╝██║🧵🔴🧵██ ██║🧵🔴🧵██ ██║
34
- ╚═══██╗██╔═══╝ ██║🧵🧵🧵██ ██║🧵🧵🧵██ ██║
35
- ██████╔╝██║ ██╚══════██ ██ ══════██ ██║
36
- ╚═════╝ ╚═╝ ╚═████████ ███████═╝ █████████╗
37
- """
38
- print(spool_art)
39
- def enter_spool_mode(
40
- npc: NPC = None,
41
- team = None,
42
- model: str = None,
43
- provider: str = None,
44
- vmodel: str = None,
45
- vprovider: str = None,
46
- attachments: List[str] = None,
47
- rag_similarity_threshold: float = 0.3,
48
- messages: List[Dict] = None,
49
- conversation_id: str = None,
50
- stream: bool = None,
51
- **kwargs,
52
- ) -> Dict:
53
- print_spool_ascii()
54
-
55
- command_history, state_team, default_npc = setup_shell()
56
-
57
-
58
- spool_state = ShellState(
59
- npc=npc or default_npc,
60
- team=team or state_team,
61
- messages=messages.copy() if messages else [],
62
- conversation_id=conversation_id or start_new_conversation(),
63
- current_path=os.getcwd(),
64
- stream_output=stream if stream is not None else initial_state.stream_output,
65
- attachments=None,
66
- )
67
-
68
-
69
- if model:
70
- spool_state.chat_model = model
71
- if provider:
72
- spool_state.chat_provider = provider
73
- if vmodel:
74
- spool_state.vision_model = vmodel
75
- if vprovider:
76
- spool_state.vision_provider = vprovider
77
9
 
78
- npc_info = f" (NPC: {spool_state.npc.name})" if spool_state.npc else ""
79
- print(f"🧵 Entering spool mode{npc_info}. Type '/sq' to exit spool mode.")
80
- print("💡 Tip: Press Ctrl+C during streaming to interrupt and continue with a new message.")
10
+ from npcsh._state import setup_shell
81
11
 
82
-
83
- loaded_chunks = {}
84
- if attachments:
85
- if isinstance(attachments, str):
86
- attachments = [f.strip() for f in attachments.split(',')]
87
-
88
- for file_path in attachments:
89
- file_path = os.path.expanduser(file_path)
90
- if not os.path.exists(file_path):
91
- print(colored(f"Error: File not found at {file_path}", "red"))
92
- continue
93
- try:
94
- chunks = load_file_contents(file_path)
95
- loaded_chunks[file_path] = chunks
96
- print(colored(f"Loaded {len(chunks)} chunks from: {file_path}", "green"))
97
- except Exception as e:
98
- print(colored(f"Error loading {file_path}: {str(e)}", "red"))
99
12
 
100
-
101
- if not spool_state.messages or spool_state.messages[0].get("role") != "system":
102
- system_message = get_system_message(spool_state.npc) if spool_state.npc else "You are a helpful assistant."
103
- spool_state.messages.insert(0, {"role": "system", "content": system_message})
104
-
105
- while True:
106
- try:
107
-
108
- npc_name = spool_state.npc.name if spool_state.npc else "chat"
109
- display_model = spool_state.npc.model if spool_state.npc and spool_state.npc.model else spool_state.chat_model
110
-
111
- prompt_str = f"{orange(npc_name)}:{display_model}🧵> "
112
- prompt = readline_safe_prompt(prompt_str)
113
- user_input = get_multiline_input(prompt).strip()
114
-
115
- if not user_input:
116
- continue
117
-
118
- if user_input.lower() == "/sq":
119
- print("Exiting spool mode.")
120
- break
121
-
122
- if user_input.lower() == "/yap":
123
- spool_state.messages = enter_yap_mode(spool_state.messages, spool_state.npc)
124
- continue
13
+ def main():
14
+ parser = argparse.ArgumentParser(description="spool - Interactive chat 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("--files", "-f", nargs="*", help="Files to load for RAG context")
18
+ parser.add_argument("--no-stream", action="store_true", help="Disable streaming")
19
+ args = parser.parse_args()
125
20
 
126
-
127
- if user_input.startswith("/ots"):
128
- command_parts = user_input.split()
129
- image_paths = []
130
-
131
- if len(command_parts) > 1:
132
- for img_path in command_parts[1:]:
133
- full_path = os.path.expanduser(img_path)
134
- if os.path.exists(full_path):
135
- image_paths.append(full_path)
136
- else:
137
- print(colored(f"Error: Image file not found at {full_path}", "red"))
138
- else:
139
- screenshot = capture_screenshot()
140
- if screenshot and "file_path" in screenshot:
141
- image_paths.append(screenshot["file_path"])
142
- print(colored(f"Screenshot captured: {screenshot['filename']}", "green"))
143
-
144
- if not image_paths:
145
- continue
146
-
147
- vision_prompt = input("Prompt for image(s) (or press Enter): ").strip() or "Describe these images."
148
-
149
-
150
- response = get_llm_response(
151
- vision_prompt,
152
- model=spool_state.vision_model,
153
- provider=spool_state.vision_provider,
154
- messages=spool_state.messages,
155
- images=image_paths,
156
- stream=spool_state.stream_output,
157
- npc=spool_state.npc,
158
- **kwargs
159
-
160
- )
161
-
162
- spool_state.messages = response.get('messages', spool_state.messages)
163
- output = response.get('response')
164
-
165
-
166
- process_result(vision_prompt, spool_state, {'output': output}, command_history)
167
- continue
168
-
169
-
170
- current_prompt = user_input
171
- if loaded_chunks:
172
- context_content = ""
173
- for filename, chunks in loaded_chunks.items():
174
- full_content_str = "\n".join(chunks)
175
- retrieved_docs = rag_search(
176
- user_input,
177
- full_content_str,
178
- similarity_threshold=rag_similarity_threshold,
179
- )
180
- if retrieved_docs:
181
- context_content += f"\n\nContext from: {filename}\n{retrieved_docs}\n"
182
-
183
- if context_content:
184
- current_prompt += f"\n\n--- Relevant context from loaded files ---\n{context_content}"
185
-
186
-
187
- response = get_llm_response(
188
- current_prompt,
189
- model=spool_state.npc.model if spool_state.npc and spool_state.npc.model else spool_state.chat_model,
190
- provider=spool_state.npc.provider if spool_state.npc and spool_state.npc.provider else spool_state.chat_provider,
191
- messages=spool_state.messages,
192
- stream=spool_state.stream_output,
193
- npc=spool_state.npc,
194
- **kwargs
195
- )
196
-
197
- spool_state.messages = response.get('messages', spool_state.messages)
198
- output = response.get('response')
199
-
200
-
201
- process_result(current_prompt, spool_state, {'output': output}, command_history)
21
+ # Setup shell to get team and default NPC
22
+ command_history, team, default_npc = setup_shell()
202
23
 
203
- except (EOFError,):
204
- print("\nExiting spool mode.")
205
- break
206
- except KeyboardInterrupt:
207
- print("\n🔄 Use '/sq' to exit or continue with a new message.")
208
- continue
24
+ if not team or "spool" not in team.jinxs_dict:
25
+ print("Error: spool jinx not found. Ensure npc_team/jinxs/modes/spool.jinx exists.")
26
+ sys.exit(1)
209
27
 
210
- return {"messages": spool_state.messages, "output": "Exited spool mode."}
28
+ # Build context for jinx execution
29
+ context = {
30
+ "npc": default_npc,
31
+ "team": team,
32
+ "messages": [],
33
+ "model": args.model,
34
+ "provider": args.provider,
35
+ "attachments": ",".join(args.files) if args.files else None,
36
+ "stream": not args.no_stream,
37
+ }
211
38
 
39
+ # Execute the jinx
40
+ spool_jinx = team.jinxs_dict["spool"]
41
+ result = spool_jinx.execute(context=context, npc=default_npc)
212
42
 
213
- def main():
214
- import argparse
215
- parser = argparse.ArgumentParser(description="Enter spool mode for chatting with an LLM")
216
- parser.add_argument("--model", help="Model to use")
217
- parser.add_argument("--provider", help="Provider to use")
218
- parser.add_argument("--attachments", nargs="*", help="Files to load into context")
219
- parser.add_argument("--stream", default="true", help="Use streaming mode")
220
- parser.add_argument("--npc", type=str, help="NPC name or path to NPC file", default='sibiji',)
221
-
222
- args = parser.parse_args()
223
-
224
-
225
- command_history, team, default_npc = setup_shell()
226
-
227
- npc = None
228
- if args.npc:
229
- if os.path.exists(os.path.expanduser(args.npc)):
230
- npc = NPC(file=args.npc)
231
- elif team and args.npc in team.npcs:
232
- npc = team.npcs[args.npc]
233
- else:
234
- try:
235
- npc_path = get_npc_path(args.npc, command_history.db_path)
236
- npc = NPC(file=npc_path)
237
- except ValueError:
238
- print(colored(f"NPC '{args.npc}' not found. Using default.", "yellow"))
239
- npc = default_npc
240
- else:
241
- npc = default_npc
43
+ if isinstance(result, dict) and result.get("output"):
44
+ print(result["output"])
242
45
 
243
- enter_spool_mode(
244
- npc=npc,
245
- team=team,
246
- model=args.model,
247
- provider=args.provider,
248
- attachments=args.attachments,
249
- stream=args.stream.lower() == "true",
250
- )
251
46
 
252
47
  if __name__ == "__main__":
253
- main()
48
+ main()