npcsh 1.1.14__py3-none-any.whl → 1.1.16__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 +533 -80
- npcsh/mcp_server.py +2 -1
- npcsh/npc.py +84 -32
- npcsh/npc_team/alicanto.npc +22 -1
- npcsh/npc_team/corca.npc +28 -9
- npcsh/npc_team/frederic.npc +25 -4
- npcsh/npc_team/guac.npc +22 -0
- npcsh/npc_team/jinxs/bin/nql.jinx +141 -0
- npcsh/npc_team/jinxs/bin/sync.jinx +230 -0
- {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/bin}/vixynt.jinx +8 -30
- npcsh/npc_team/jinxs/bin/wander.jinx +152 -0
- npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +220 -0
- npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +40 -0
- npcsh/npc_team/jinxs/lib/browser/close_browser.jinx +14 -0
- npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +43 -0
- npcsh/npc_team/jinxs/lib/computer_use/click.jinx +23 -0
- npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +26 -0
- npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +37 -0
- npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +23 -0
- npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +27 -0
- npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +21 -0
- {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/edit_file.jinx +3 -3
- {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/load_file.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/paste.jinx +134 -0
- {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/search.jinx +2 -1
- npcsh/npc_team/jinxs/{code → lib/core}/sh.jinx +2 -8
- npcsh/npc_team/jinxs/{code → lib/core}/sql.jinx +1 -1
- npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +232 -0
- npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +184 -0
- npcsh/npc_team/jinxs/lib/research/arxiv.jinx +76 -0
- npcsh/npc_team/jinxs/lib/research/paper_search.jinx +101 -0
- npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +69 -0
- npcsh/npc_team/jinxs/{utils/core → lib/utils}/build.jinx +8 -8
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +176 -0
- npcsh/npc_team/jinxs/lib/utils/shh.jinx +17 -0
- npcsh/npc_team/jinxs/lib/utils/switch.jinx +62 -0
- npcsh/npc_team/jinxs/lib/utils/switches.jinx +61 -0
- npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +205 -0
- npcsh/npc_team/jinxs/lib/utils/verbose.jinx +17 -0
- npcsh/npc_team/kadiefa.npc +19 -1
- npcsh/npc_team/plonk.npc +26 -1
- npcsh/npc_team/plonkjr.npc +22 -1
- npcsh/npc_team/sibiji.npc +23 -2
- npcsh/npcsh.py +153 -39
- npcsh/ui.py +22 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/alicanto.npc +23 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/arxiv.jinx +76 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/browser_action.jinx +220 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/browser_screenshot.jinx +40 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/build.jinx +8 -8
- npcsh-1.1.16.data/data/npcsh/npc_team/click.jinx +23 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/close_browser.jinx +14 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/convene.jinx +232 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/corca.npc +31 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/delegate.jinx +184 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/edit_file.jinx +3 -3
- npcsh-1.1.16.data/data/npcsh/npc_team/frederic.npc +27 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/guac.npc +22 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/jinxs.jinx +176 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/kadiefa.npc +21 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/key_press.jinx +26 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/launch_app.jinx +37 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/load_file.jinx +1 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/nql.jinx +141 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/open_browser.jinx +43 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/paper_search.jinx +101 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/paste.jinx +134 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/plonk.npc +27 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/plonkjr.npc +23 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/screenshot.jinx +23 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/search.jinx +2 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/semantic_scholar.jinx +69 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sh.jinx +2 -8
- npcsh-1.1.16.data/data/npcsh/npc_team/shh.jinx +17 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/sibiji.npc +24 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sql.jinx +1 -1
- npcsh-1.1.16.data/data/npcsh/npc_team/switch.jinx +62 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/switches.jinx +61 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/sync.jinx +230 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/teamviz.jinx +205 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/type_text.jinx +27 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/verbose.jinx +17 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/vixynt.jinx +8 -30
- npcsh-1.1.16.data/data/npcsh/npc_team/wait.jinx +21 -0
- npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +152 -0
- {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/METADATA +399 -58
- npcsh-1.1.16.dist-info/RECORD +170 -0
- npcsh-1.1.16.dist-info/entry_points.txt +19 -0
- npcsh-1.1.16.dist-info/top_level.txt +2 -0
- project/__init__.py +1 -0
- npcsh/npc_team/foreman.npc +0 -7
- npcsh/npc_team/jinxs/modes/alicanto.jinx +0 -194
- npcsh/npc_team/jinxs/modes/corca.jinx +0 -249
- npcsh/npc_team/jinxs/modes/guac.jinx +0 -317
- npcsh/npc_team/jinxs/modes/plonk.jinx +0 -214
- npcsh/npc_team/jinxs/modes/pti.jinx +0 -170
- npcsh/npc_team/jinxs/modes/wander.jinx +0 -186
- npcsh/npc_team/jinxs/utils/agent.jinx +0 -17
- npcsh/npc_team/jinxs/utils/core/jinxs.jinx +0 -32
- npcsh-1.1.14.data/data/npcsh/npc_team/agent.jinx +0 -17
- npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.jinx +0 -194
- npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.npc +0 -2
- npcsh-1.1.14.data/data/npcsh/npc_team/corca.jinx +0 -249
- npcsh-1.1.14.data/data/npcsh/npc_team/corca.npc +0 -12
- npcsh-1.1.14.data/data/npcsh/npc_team/foreman.npc +0 -7
- npcsh-1.1.14.data/data/npcsh/npc_team/frederic.npc +0 -6
- npcsh-1.1.14.data/data/npcsh/npc_team/guac.jinx +0 -317
- npcsh-1.1.14.data/data/npcsh/npc_team/jinxs.jinx +0 -32
- npcsh-1.1.14.data/data/npcsh/npc_team/kadiefa.npc +0 -3
- npcsh-1.1.14.data/data/npcsh/npc_team/plonk.jinx +0 -214
- npcsh-1.1.14.data/data/npcsh/npc_team/plonk.npc +0 -2
- npcsh-1.1.14.data/data/npcsh/npc_team/plonkjr.npc +0 -2
- npcsh-1.1.14.data/data/npcsh/npc_team/pti.jinx +0 -170
- npcsh-1.1.14.data/data/npcsh/npc_team/sibiji.npc +0 -3
- npcsh-1.1.14.data/data/npcsh/npc_team/wander.jinx +0 -186
- npcsh-1.1.14.dist-info/RECORD +0 -135
- npcsh-1.1.14.dist-info/entry_points.txt +0 -9
- npcsh-1.1.14.dist-info/top_level.txt +0 -1
- /npcsh/npc_team/jinxs/{utils → bin}/roll.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → bin}/sample.jinx +0 -0
- /npcsh/npc_team/jinxs/{modes → bin}/spool.jinx +0 -0
- /npcsh/npc_team/jinxs/{modes → bin}/yap.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/computer_use}/trigger.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/chat.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/cmd.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/compress.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/ots.jinx +0 -0
- /npcsh/npc_team/jinxs/{code → lib/core}/python.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/core}/sleep.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils/core → lib/utils}/compile.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils/core → lib/utils}/help.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils/core → lib/utils}/init.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/utils}/serve.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils/core → lib/utils}/set.jinx +0 -0
- /npcsh/npc_team/jinxs/{utils → lib/utils}/usage.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/yap.jinx +0 -0
- {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/WHEEL +0 -0
- {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/licenses/LICENSE +0 -0
npcsh/mcp_server.py
CHANGED
|
@@ -30,7 +30,8 @@ from typing import Optional, Dict, Any, List, Union, Callable, get_type_hints
|
|
|
30
30
|
from functools import wraps
|
|
31
31
|
import sys
|
|
32
32
|
|
|
33
|
-
from npcpy.llm_funcs import generate_group_candidates, abstract,
|
|
33
|
+
from npcpy.llm_funcs import generate_group_candidates, abstract,
|
|
34
|
+
zoom_in, execute_llm_command, gen_image
|
|
34
35
|
from npcpy.memory.search import search_similar_texts, execute_search_command, execute_rag_command, answer_with_rag, execute_brainblast_command
|
|
35
36
|
from npcpy.data.load import load_file_contents
|
|
36
37
|
from npcpy.memory.command_history import CommandHistory
|
npcsh/npc.py
CHANGED
|
@@ -95,9 +95,20 @@ def main():
|
|
|
95
95
|
global_model = args.model
|
|
96
96
|
global_provider = args.provider
|
|
97
97
|
|
|
98
|
+
# Load team early so we can check for jinxs
|
|
99
|
+
try:
|
|
100
|
+
command_history, team, forenpc_obj = setup_shell()
|
|
101
|
+
# Load jinxs into router so they're recognized as commands
|
|
102
|
+
from npcsh._state import initialize_router_with_jinxs
|
|
103
|
+
initialize_router_with_jinxs(team, router)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(f"Warning: Could not set up full npcsh environment: {e}", file=sys.stderr)
|
|
106
|
+
team = None
|
|
107
|
+
forenpc_obj = None
|
|
108
|
+
|
|
98
109
|
is_valid_command = False
|
|
99
110
|
command_name = None
|
|
100
|
-
|
|
111
|
+
|
|
101
112
|
if all_args:
|
|
102
113
|
first_arg = all_args[0]
|
|
103
114
|
if first_arg.startswith('/'):
|
|
@@ -145,15 +156,8 @@ def main():
|
|
|
145
156
|
if args.provider is None:
|
|
146
157
|
args.provider = global_provider
|
|
147
158
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
except Exception as e:
|
|
151
|
-
print(
|
|
152
|
-
f"Warning: Could not set up full npcsh environment: {e}",
|
|
153
|
-
file=sys.stderr
|
|
154
|
-
)
|
|
155
|
-
print("Falling back to basic NPC loading...", file=sys.stderr)
|
|
156
|
-
team = None
|
|
159
|
+
# Team already loaded above, just set up NPC
|
|
160
|
+
if not forenpc_obj:
|
|
157
161
|
forenpc_obj = load_npc_by_name(args.npc, NPCSH_DB_PATH)
|
|
158
162
|
|
|
159
163
|
npc_instance = None
|
|
@@ -173,25 +177,6 @@ def main():
|
|
|
173
177
|
print(f"Error: Could not load NPC '{args.npc}'", file=sys.stderr)
|
|
174
178
|
sys.exit(1)
|
|
175
179
|
|
|
176
|
-
if not is_valid_command and all_args:
|
|
177
|
-
first_arg = all_args[0]
|
|
178
|
-
|
|
179
|
-
jinx_found = False
|
|
180
|
-
if team and first_arg in team.jinxs_dict:
|
|
181
|
-
jinx_found = True
|
|
182
|
-
elif (
|
|
183
|
-
isinstance(npc_instance, NPC)
|
|
184
|
-
and hasattr(npc_instance, 'jinxs_dict')
|
|
185
|
-
and first_arg in npc_instance.jinxs_dict
|
|
186
|
-
):
|
|
187
|
-
jinx_found = True
|
|
188
|
-
|
|
189
|
-
if jinx_found:
|
|
190
|
-
is_valid_command = True
|
|
191
|
-
command_name = '/' + first_arg
|
|
192
|
-
all_args = all_args[1:]
|
|
193
|
-
unknown_args = all_args
|
|
194
|
-
|
|
195
180
|
shell_state = initial_state
|
|
196
181
|
shell_state.npc = npc_instance
|
|
197
182
|
shell_state.team = team
|
|
@@ -245,12 +230,13 @@ def main():
|
|
|
245
230
|
)
|
|
246
231
|
|
|
247
232
|
if (
|
|
248
|
-
NPCSH_STREAM_OUTPUT
|
|
233
|
+
NPCSH_STREAM_OUTPUT
|
|
234
|
+
and output is not None
|
|
249
235
|
and not isinstance(output, str)
|
|
250
236
|
):
|
|
251
237
|
print_and_process_stream_with_markdown(
|
|
252
|
-
output,
|
|
253
|
-
model_for_stream,
|
|
238
|
+
output,
|
|
239
|
+
model_for_stream,
|
|
254
240
|
provider_for_stream
|
|
255
241
|
)
|
|
256
242
|
elif output is not None:
|
|
@@ -319,5 +305,71 @@ def main():
|
|
|
319
305
|
sys.exit(1)
|
|
320
306
|
|
|
321
307
|
|
|
308
|
+
def jinx_main():
|
|
309
|
+
"""Entry point for bin jinxs called directly from CLI.
|
|
310
|
+
|
|
311
|
+
Parses arguments as key=value pairs and executes the jinx.
|
|
312
|
+
Example: nql show=1 model=daily_summary
|
|
313
|
+
"""
|
|
314
|
+
import os
|
|
315
|
+
|
|
316
|
+
# Get jinx name from command name
|
|
317
|
+
jinx_name = os.path.basename(sys.argv[0])
|
|
318
|
+
|
|
319
|
+
# Parse remaining args as key=value pairs
|
|
320
|
+
args = sys.argv[1:]
|
|
321
|
+
jinx_args = []
|
|
322
|
+
|
|
323
|
+
for arg in args:
|
|
324
|
+
if arg in ['-h', '--help']:
|
|
325
|
+
print(f"Usage: {jinx_name} [key=value ...]")
|
|
326
|
+
print(f"\nRun the '{jinx_name}' jinx with specified parameters.")
|
|
327
|
+
print(f"\nExamples:")
|
|
328
|
+
print(f" {jinx_name} show=1")
|
|
329
|
+
print(f" {jinx_name} model=my_model db=~/mydb.db")
|
|
330
|
+
print(f"\nOr use: npc {jinx_name} [key=value ...]")
|
|
331
|
+
sys.exit(0)
|
|
332
|
+
jinx_args.append(arg)
|
|
333
|
+
|
|
334
|
+
# Build command string
|
|
335
|
+
if jinx_args:
|
|
336
|
+
command = f"/{jinx_name} " + " ".join(jinx_args)
|
|
337
|
+
else:
|
|
338
|
+
command = f"/{jinx_name}"
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
_, team, forenpc_obj = setup_shell()
|
|
342
|
+
|
|
343
|
+
from npcsh._state import initialize_router_with_jinxs
|
|
344
|
+
initialize_router_with_jinxs(team, router)
|
|
345
|
+
|
|
346
|
+
# Update the global initial_state with team/npc context
|
|
347
|
+
initial_state.team = team
|
|
348
|
+
initial_state.npc = forenpc_obj
|
|
349
|
+
if forenpc_obj:
|
|
350
|
+
initial_state.chat_model = forenpc_obj.model or NPCSH_CHAT_MODEL
|
|
351
|
+
initial_state.chat_provider = forenpc_obj.provider or NPCSH_CHAT_PROVIDER
|
|
352
|
+
|
|
353
|
+
_, result = execute_slash_command(
|
|
354
|
+
command,
|
|
355
|
+
stdin_input=None,
|
|
356
|
+
state=initial_state,
|
|
357
|
+
stream=NPCSH_STREAM_OUTPUT,
|
|
358
|
+
router=router
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if isinstance(result, dict):
|
|
362
|
+
output = result.get("output") or result.get("response")
|
|
363
|
+
if output is not None:
|
|
364
|
+
render_markdown(str(output))
|
|
365
|
+
elif result is not None:
|
|
366
|
+
render_markdown(str(result))
|
|
367
|
+
|
|
368
|
+
except Exception as e:
|
|
369
|
+
print(f"Error executing jinx '{jinx_name}': {e}", file=sys.stderr)
|
|
370
|
+
traceback.print_exc()
|
|
371
|
+
sys.exit(1)
|
|
372
|
+
|
|
373
|
+
|
|
322
374
|
if __name__ == "__main__":
|
|
323
375
|
main()
|
npcsh/npc_team/alicanto.npc
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
1
|
name: alicanto
|
|
2
|
-
|
|
2
|
+
ascii_art: |
|
|
3
|
+
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
|
|
4
|
+
█████ ██ ██ ██████ █████ ███ ██ ████████ ██████
|
|
5
|
+
██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██
|
|
6
|
+
███████ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ 🦅
|
|
7
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
8
|
+
██ ██ ███████ ██ ██████ ██ ██ ██ ████ ██ ██████
|
|
9
|
+
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
|
|
10
|
+
colors:
|
|
11
|
+
top: "255,215,0"
|
|
12
|
+
bottom: "218,165,32"
|
|
13
|
+
primary_directive: |
|
|
14
|
+
You are alicanto, the research and exploration specialist of the NPC team.
|
|
15
|
+
Like the mythical bird, you lead users to discover valuable information.
|
|
16
|
+
Your role is web research, searching, and helping users explore topics.
|
|
17
|
+
Use search tools to find information and present findings clearly.
|
|
18
|
+
jinxs:
|
|
19
|
+
- lib/core/search
|
|
20
|
+
- lib/core/sh
|
|
21
|
+
- lib/core/python
|
|
22
|
+
- lib/core/load_file
|
|
23
|
+
- lib/research/*
|
npcsh/npc_team/corca.npc
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
name: corca
|
|
2
|
+
ascii_art: |
|
|
3
|
+
██████ ██████ ██████ ██████ ██████
|
|
4
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██🦌🦌██
|
|
5
|
+
██ ██ ██ ██ ██ ██ ██🦌🦌██
|
|
6
|
+
██ ██ ██ ████████ ██ ████████
|
|
7
|
+
██ ██ ██ ██ ███ ██ ██ ██
|
|
8
|
+
██ ██ ██ ██ ██ ███ ██ ██ ██ ██
|
|
9
|
+
██████ ██████ ██ ███ ██████ ██ ██
|
|
10
|
+
colors:
|
|
11
|
+
top: "64,224,208"
|
|
12
|
+
bottom: "255,165,0"
|
|
2
13
|
primary_directive: |
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
You are corca, the software development specialist of the NPC team.
|
|
15
|
+
Your expertise is in writing, reviewing, and debugging code.
|
|
16
|
+
You think through problems carefully and favor solutions that prioritize simplicity and clarity.
|
|
17
|
+
Always consider how suggestions may increase rather than reduce tech debt unnecessarily.
|
|
18
|
+
When in doubt, ask for clarification with concrete options that make it easy for users to choose.
|
|
19
|
+
|
|
20
|
+
CRITICAL: You MUST ALWAYS use FULL ABSOLUTE PATHS for all file operations.
|
|
21
|
+
- NEVER use relative paths like "apps/api" or "./src"
|
|
22
|
+
- ALWAYS expand paths starting from root like "/Users/username/project/apps/api"
|
|
23
|
+
- When given a task, first determine the absolute path of the working directory using pwd
|
|
24
|
+
- Prefix all file paths with the full absolute path
|
|
25
|
+
jinxs:
|
|
26
|
+
- lib/core/sh
|
|
27
|
+
- lib/core/python
|
|
28
|
+
- lib/core/edit_file
|
|
29
|
+
- lib/core/load_file
|
|
30
|
+
- lib/core/search
|
|
12
31
|
|
npcsh/npc_team/frederic.npc
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
name: frederic
|
|
2
|
+
ascii_art: |
|
|
3
|
+
❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️
|
|
4
|
+
███████ ██████ ███████ ██████ ███████ ██████ ██ ██████
|
|
5
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
6
|
+
█████ ██████ █████ ██ ██ █████ ██████ ██ ██ 🐻❄️
|
|
7
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
8
|
+
██ ██ ██ ███████ ██████ ███████ ██ ██ ██ ██████
|
|
9
|
+
❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️
|
|
10
|
+
colors:
|
|
11
|
+
top: "224,255,255"
|
|
12
|
+
bottom: "173,216,230"
|
|
2
13
|
primary_directive: |
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
14
|
+
You are frederic the polar bear - a fusion of Richard Feynman and Frederic Chopin.
|
|
15
|
+
You have Feynman's playful curiosity, his ability to explain complex physics simply,
|
|
16
|
+
and his irreverent wit. You also have Chopin's romantic soul, his passion for music,
|
|
17
|
+
and his ability to find beauty in mathematical structures.
|
|
18
|
+
You help users with hard math, physics problems, and music composition.
|
|
19
|
+
Cut through the ice to get to what matters. Make the complex feel simple and beautiful.
|
|
20
|
+
jinxs:
|
|
21
|
+
- lib/core/python
|
|
22
|
+
- lib/core/sql
|
|
23
|
+
- lib/core/sh
|
|
24
|
+
- lib/core/load_file
|
|
25
|
+
- lib/core/search
|
|
26
|
+
- lib/gen/*
|
|
27
|
+
- bin/wander
|
npcsh/npc_team/guac.npc
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: guac
|
|
2
|
+
ascii_art: |
|
|
3
|
+
🟢🟢🟢🟢🟢
|
|
4
|
+
🟢 🟢
|
|
5
|
+
🟢
|
|
6
|
+
🟢
|
|
7
|
+
🟢
|
|
8
|
+
🟢 🟢🟢🟢 🟢 🟢 🟢🟢🟢 🟢🟢🟢
|
|
9
|
+
🟢 🟢 🟢 🟢 ⚫⚫🟢 🟢
|
|
10
|
+
🟢 🟢 🟢 🟢 ⚫🥑🧅⚫ 🟢
|
|
11
|
+
🟢 🟢 🟢 🟢 ⚫🥑🍅⚫ 🟢
|
|
12
|
+
🟢🟢🟢🟢🟢🟢 🟢🟢🟢🟢 ⚫⚫🟢 🟢🟢🟢
|
|
13
|
+
primary_directive: |
|
|
14
|
+
You are guac, the data analysis specialist of the NPC team.
|
|
15
|
+
Your expertise is in loading, analyzing, and visualizing data.
|
|
16
|
+
You work with pandas DataFrames, numpy arrays, and matplotlib plots.
|
|
17
|
+
Help users load data files, run Python code for analysis, and create visualizations.
|
|
18
|
+
jinxs:
|
|
19
|
+
- lib/core/python
|
|
20
|
+
- lib/core/sql
|
|
21
|
+
- lib/core/sh
|
|
22
|
+
- lib/core/load_file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
jinx_name: nql
|
|
2
|
+
description: "Run NPC-SQL models with AI-powered transformations. Supports cron scheduling."
|
|
3
|
+
inputs:
|
|
4
|
+
- models_dir: "~/.npcsh/npc_team/models"
|
|
5
|
+
- db: "~/npcsh_history.db"
|
|
6
|
+
- model: ""
|
|
7
|
+
- schema: ""
|
|
8
|
+
- show: ""
|
|
9
|
+
- cron: ""
|
|
10
|
+
- install_cron: ""
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: run_nql
|
|
14
|
+
engine: python
|
|
15
|
+
code: |
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
models_dir = context.get('models_dir') or '~/.npcsh/npc_team/models'
|
|
21
|
+
db_path = context.get('db') or '~/npcsh_history.db'
|
|
22
|
+
model_name = context.get('model') or ''
|
|
23
|
+
schema = context.get('schema') or ''
|
|
24
|
+
list_models = context.get('show') or ''
|
|
25
|
+
cron_expr = context.get('cron') or ''
|
|
26
|
+
install_cron = context.get('install_cron') or ''
|
|
27
|
+
|
|
28
|
+
models_dir = os.path.expanduser(models_dir)
|
|
29
|
+
db_path = os.path.expanduser(db_path)
|
|
30
|
+
|
|
31
|
+
# Find NPC team directory
|
|
32
|
+
npc_dir = None
|
|
33
|
+
if os.path.exists('./npc_team'):
|
|
34
|
+
npc_dir = './npc_team'
|
|
35
|
+
elif os.path.exists(os.path.expanduser('~/.npcsh/npc_team')):
|
|
36
|
+
npc_dir = os.path.expanduser('~/.npcsh/npc_team')
|
|
37
|
+
|
|
38
|
+
# Import npcsql
|
|
39
|
+
try:
|
|
40
|
+
from npcpy.sql.npcsql import ModelCompiler, SQLModel
|
|
41
|
+
except ImportError:
|
|
42
|
+
output = "Error: npcpy.sql.npcsql not found. Install npcpy first."
|
|
43
|
+
raise SystemExit(1)
|
|
44
|
+
|
|
45
|
+
# List models mode
|
|
46
|
+
if list_models:
|
|
47
|
+
if not os.path.exists(models_dir):
|
|
48
|
+
output = "No models directory found at " + models_dir
|
|
49
|
+
else:
|
|
50
|
+
models_path = Path(models_dir)
|
|
51
|
+
sql_files = list(models_path.glob("**/*.sql"))
|
|
52
|
+
if not sql_files:
|
|
53
|
+
output = "No .sql models found in " + models_dir
|
|
54
|
+
else:
|
|
55
|
+
lines = ["Available NQL models in " + models_dir + ":", ""]
|
|
56
|
+
for f in sorted(sql_files):
|
|
57
|
+
rel = f.relative_to(models_path)
|
|
58
|
+
# Check for nql functions
|
|
59
|
+
with open(f, 'r') as fh:
|
|
60
|
+
content = fh.read()
|
|
61
|
+
has_nql = "nql." in content
|
|
62
|
+
marker = " [NQL]" if has_nql else ""
|
|
63
|
+
lines.append(" " + str(rel) + marker)
|
|
64
|
+
output = "\n".join(lines)
|
|
65
|
+
|
|
66
|
+
# Install cron mode
|
|
67
|
+
elif install_cron:
|
|
68
|
+
import subprocess
|
|
69
|
+
# Parse cron expression and model
|
|
70
|
+
parts = install_cron.split()
|
|
71
|
+
if len(parts) < 5:
|
|
72
|
+
output = "Error: cron expression must have 5 fields (min hour day month weekday)"
|
|
73
|
+
else:
|
|
74
|
+
cron_time = " ".join(parts[:5])
|
|
75
|
+
cron_model = parts[5] if len(parts) > 5 else ""
|
|
76
|
+
|
|
77
|
+
# Build command
|
|
78
|
+
nql_cmd = "npc nql"
|
|
79
|
+
if cron_model:
|
|
80
|
+
nql_cmd += " model=" + cron_model
|
|
81
|
+
nql_cmd += " models_dir=" + models_dir
|
|
82
|
+
nql_cmd += " db=" + db_path
|
|
83
|
+
if schema:
|
|
84
|
+
nql_cmd += " schema=" + schema
|
|
85
|
+
|
|
86
|
+
cron_line = cron_time + " " + nql_cmd
|
|
87
|
+
|
|
88
|
+
# Get current crontab
|
|
89
|
+
try:
|
|
90
|
+
result = subprocess.run(['crontab', '-l'], capture_output=True, text=True)
|
|
91
|
+
current = result.stdout if result.returncode == 0 else ""
|
|
92
|
+
except:
|
|
93
|
+
current = ""
|
|
94
|
+
|
|
95
|
+
# Check if already installed
|
|
96
|
+
if nql_cmd in current:
|
|
97
|
+
output = "Cron job already exists for: " + nql_cmd
|
|
98
|
+
else:
|
|
99
|
+
# Add new cron line
|
|
100
|
+
new_crontab = current.rstrip() + "\n" + cron_line + "\n"
|
|
101
|
+
proc = subprocess.run(['crontab', '-'], input=new_crontab, text=True, capture_output=True)
|
|
102
|
+
if proc.returncode == 0:
|
|
103
|
+
output = "Installed cron job:\n " + cron_line
|
|
104
|
+
else:
|
|
105
|
+
output = "Failed to install cron: " + proc.stderr
|
|
106
|
+
|
|
107
|
+
# Run models mode
|
|
108
|
+
else:
|
|
109
|
+
if not os.path.exists(models_dir):
|
|
110
|
+
output = "Models directory not found: " + models_dir + "\nCreate it with: mkdir -p " + models_dir
|
|
111
|
+
else:
|
|
112
|
+
try:
|
|
113
|
+
compiler = ModelCompiler(
|
|
114
|
+
models_dir=models_dir,
|
|
115
|
+
target_engine=db_path,
|
|
116
|
+
npc_directory=npc_dir,
|
|
117
|
+
target_schema=schema if schema else None
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if model_name:
|
|
121
|
+
# Run specific model
|
|
122
|
+
compiler.discover_models()
|
|
123
|
+
if model_name not in compiler.models:
|
|
124
|
+
available = list(compiler.models.keys())
|
|
125
|
+
output = "Model '" + model_name + "' not found. Available: " + str(available)
|
|
126
|
+
else:
|
|
127
|
+
print("Running model: " + model_name)
|
|
128
|
+
df = compiler.execute_model(model_name)
|
|
129
|
+
output = "Model '" + model_name + "' completed. Rows: " + str(len(df))
|
|
130
|
+
else:
|
|
131
|
+
# Run all models in dependency order
|
|
132
|
+
results = compiler.run_all_models()
|
|
133
|
+
lines = ["NQL run complete:", ""]
|
|
134
|
+
for name, df in results.items():
|
|
135
|
+
lines.append(" " + name + ": " + str(len(df)) + " rows")
|
|
136
|
+
output = "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
output = "NQL Error: " + str(e)
|
|
140
|
+
import traceback
|
|
141
|
+
traceback.print_exc()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
jinx_name: "sync"
|
|
2
|
+
description: "Sync npc_team files from the npcsh repo to ~/.npcsh/npc_team. Detects local modifications before overwriting."
|
|
3
|
+
inputs:
|
|
4
|
+
- force: "" # Use --force or -f to overwrite all files without prompting
|
|
5
|
+
- dry_run: "" # Use --dry-run or -d to preview changes without applying them
|
|
6
|
+
- jinxs: "" # Use --jinxs to sync only .jinx files
|
|
7
|
+
- npcs: "" # Use --npcs to sync only .npc files
|
|
8
|
+
- ctx: "" # Use --ctx to sync only .ctx files
|
|
9
|
+
- images: "" # Use --images to sync only image files (.png, .jpg, .jpeg)
|
|
10
|
+
steps:
|
|
11
|
+
- name: "sync_npc_team"
|
|
12
|
+
engine: "python"
|
|
13
|
+
code: |
|
|
14
|
+
import os
|
|
15
|
+
import hashlib
|
|
16
|
+
import shutil
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
|
|
20
|
+
force = context.get('force', False)
|
|
21
|
+
dry_run = context.get('dry_run', False)
|
|
22
|
+
sync_jinxs = context.get('jinxs', False)
|
|
23
|
+
sync_npcs = context.get('npcs', False)
|
|
24
|
+
sync_ctx = context.get('ctx', False)
|
|
25
|
+
sync_images = context.get('images', False)
|
|
26
|
+
|
|
27
|
+
# Convert string flags to boolean
|
|
28
|
+
def to_bool(val):
|
|
29
|
+
if isinstance(val, bool):
|
|
30
|
+
return val
|
|
31
|
+
if isinstance(val, str):
|
|
32
|
+
return val.lower() in ('true', '1', 'yes', 'y')
|
|
33
|
+
return bool(val)
|
|
34
|
+
|
|
35
|
+
force = to_bool(force)
|
|
36
|
+
dry_run = to_bool(dry_run)
|
|
37
|
+
sync_jinxs = to_bool(sync_jinxs)
|
|
38
|
+
sync_npcs = to_bool(sync_npcs)
|
|
39
|
+
sync_ctx = to_bool(sync_ctx)
|
|
40
|
+
sync_images = to_bool(sync_images)
|
|
41
|
+
|
|
42
|
+
# If none specified, sync all
|
|
43
|
+
sync_all = not (sync_jinxs or sync_npcs or sync_ctx or sync_images)
|
|
44
|
+
|
|
45
|
+
# Find the repo npc_team directory
|
|
46
|
+
# Check common locations
|
|
47
|
+
possible_repo_paths = [
|
|
48
|
+
Path.home() / "npcww" / "npc-core" / "npcsh" / "npcsh" / "npc_team",
|
|
49
|
+
Path.home() / "npc-core" / "npcsh" / "npcsh" / "npc_team",
|
|
50
|
+
Path.home() / "repos" / "npcsh" / "npcsh" / "npc_team",
|
|
51
|
+
Path.home() / "Projects" / "npcsh" / "npcsh" / "npc_team",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Also check if we can find it via pip show
|
|
55
|
+
try:
|
|
56
|
+
import subprocess
|
|
57
|
+
result = subprocess.run(['pip', 'show', 'npcsh', '-f'], capture_output=True, text=True)
|
|
58
|
+
if result.returncode == 0:
|
|
59
|
+
for line in result.stdout.split('\n'):
|
|
60
|
+
if line.startswith('Location:'):
|
|
61
|
+
pip_path = Path(line.split(':', 1)[1].strip()) / "npcsh" / "npc_team"
|
|
62
|
+
if pip_path.exists():
|
|
63
|
+
possible_repo_paths.insert(0, pip_path)
|
|
64
|
+
except:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
repo_npc_team = None
|
|
68
|
+
for path in possible_repo_paths:
|
|
69
|
+
if path.exists() and path.is_dir():
|
|
70
|
+
repo_npc_team = path
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
local_npc_team = Path.home() / ".npcsh" / "npc_team"
|
|
74
|
+
|
|
75
|
+
if not repo_npc_team:
|
|
76
|
+
context['output'] = "Error: Could not find npcsh repo npc_team directory.\nSearched:\n" + "\n".join(f" - {p}" for p in possible_repo_paths)
|
|
77
|
+
exit()
|
|
78
|
+
|
|
79
|
+
if not local_npc_team.exists():
|
|
80
|
+
context['output'] = f"Error: Local npc_team directory not found at {local_npc_team}"
|
|
81
|
+
exit()
|
|
82
|
+
|
|
83
|
+
def get_file_hash(filepath):
|
|
84
|
+
"""Get MD5 hash of file contents."""
|
|
85
|
+
try:
|
|
86
|
+
with open(filepath, 'rb') as f:
|
|
87
|
+
return hashlib.md5(f.read()).hexdigest()
|
|
88
|
+
except:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def get_files_recursive(base_path, extensions=None):
|
|
92
|
+
"""Get all files recursively, optionally filtered by extensions."""
|
|
93
|
+
files = []
|
|
94
|
+
for root, dirs, filenames in os.walk(base_path):
|
|
95
|
+
# Skip .git directories
|
|
96
|
+
dirs[:] = [d for d in dirs if d != '.git']
|
|
97
|
+
for filename in filenames:
|
|
98
|
+
if filename.startswith('.'):
|
|
99
|
+
continue
|
|
100
|
+
if extensions and not any(filename.endswith(ext) for ext in extensions):
|
|
101
|
+
continue
|
|
102
|
+
full_path = Path(root) / filename
|
|
103
|
+
rel_path = full_path.relative_to(base_path)
|
|
104
|
+
files.append(rel_path)
|
|
105
|
+
return files
|
|
106
|
+
|
|
107
|
+
# Build list of extensions to sync based on flags
|
|
108
|
+
sync_extensions = []
|
|
109
|
+
if sync_all or sync_npcs:
|
|
110
|
+
sync_extensions.append('.npc')
|
|
111
|
+
if sync_all or sync_ctx:
|
|
112
|
+
sync_extensions.append('.ctx')
|
|
113
|
+
if sync_all or sync_jinxs:
|
|
114
|
+
sync_extensions.append('.jinx')
|
|
115
|
+
if sync_all or sync_images:
|
|
116
|
+
sync_extensions.extend(['.png', '.jpg', '.jpeg'])
|
|
117
|
+
|
|
118
|
+
# Get files from repo
|
|
119
|
+
repo_files = get_files_recursive(repo_npc_team, sync_extensions)
|
|
120
|
+
|
|
121
|
+
output_lines = []
|
|
122
|
+
output_lines.append(f"Syncing from: {repo_npc_team}")
|
|
123
|
+
output_lines.append(f"Syncing to: {local_npc_team}")
|
|
124
|
+
|
|
125
|
+
# Show what's being synced
|
|
126
|
+
sync_types = []
|
|
127
|
+
if sync_all:
|
|
128
|
+
sync_types.append("all")
|
|
129
|
+
else:
|
|
130
|
+
if sync_npcs: sync_types.append("npcs")
|
|
131
|
+
if sync_ctx: sync_types.append("ctx")
|
|
132
|
+
if sync_jinxs: sync_types.append("jinxs")
|
|
133
|
+
if sync_images: sync_types.append("images")
|
|
134
|
+
output_lines.append(f"Syncing: {', '.join(sync_types)}")
|
|
135
|
+
|
|
136
|
+
if dry_run:
|
|
137
|
+
output_lines.append("\n[DRY RUN - No changes will be made]\n")
|
|
138
|
+
output_lines.append("")
|
|
139
|
+
|
|
140
|
+
new_files = []
|
|
141
|
+
updated_files = []
|
|
142
|
+
modified_locally = []
|
|
143
|
+
unchanged_files = []
|
|
144
|
+
|
|
145
|
+
for rel_path in repo_files:
|
|
146
|
+
repo_file = repo_npc_team / rel_path
|
|
147
|
+
local_file = local_npc_team / rel_path
|
|
148
|
+
|
|
149
|
+
if not local_file.exists():
|
|
150
|
+
new_files.append(rel_path)
|
|
151
|
+
else:
|
|
152
|
+
repo_hash = get_file_hash(repo_file)
|
|
153
|
+
local_hash = get_file_hash(local_file)
|
|
154
|
+
|
|
155
|
+
if repo_hash == local_hash:
|
|
156
|
+
unchanged_files.append(rel_path)
|
|
157
|
+
else:
|
|
158
|
+
# Check if local file is newer (possibly modified by user)
|
|
159
|
+
repo_mtime = repo_file.stat().st_mtime
|
|
160
|
+
local_mtime = local_file.stat().st_mtime
|
|
161
|
+
|
|
162
|
+
if local_mtime > repo_mtime:
|
|
163
|
+
modified_locally.append((rel_path, local_mtime, repo_mtime))
|
|
164
|
+
else:
|
|
165
|
+
updated_files.append(rel_path)
|
|
166
|
+
|
|
167
|
+
# Report findings
|
|
168
|
+
if new_files:
|
|
169
|
+
output_lines.append(f"New files to add ({len(new_files)}):")
|
|
170
|
+
for f in new_files:
|
|
171
|
+
output_lines.append(f" + {f}")
|
|
172
|
+
output_lines.append("")
|
|
173
|
+
|
|
174
|
+
if updated_files:
|
|
175
|
+
output_lines.append(f"Files to update ({len(updated_files)}):")
|
|
176
|
+
for f in updated_files:
|
|
177
|
+
output_lines.append(f" ~ {f}")
|
|
178
|
+
output_lines.append("")
|
|
179
|
+
|
|
180
|
+
if modified_locally:
|
|
181
|
+
output_lines.append(f"Locally modified files ({len(modified_locally)}):")
|
|
182
|
+
for f, local_t, repo_t in modified_locally:
|
|
183
|
+
local_dt = datetime.fromtimestamp(local_t).strftime('%Y-%m-%d %H:%M')
|
|
184
|
+
repo_dt = datetime.fromtimestamp(repo_t).strftime('%Y-%m-%d %H:%M')
|
|
185
|
+
output_lines.append(f" ! {f}")
|
|
186
|
+
output_lines.append(f" local: {local_dt} repo: {repo_dt}")
|
|
187
|
+
if not force:
|
|
188
|
+
output_lines.append(" (use --force to overwrite these)")
|
|
189
|
+
output_lines.append("")
|
|
190
|
+
|
|
191
|
+
if unchanged_files:
|
|
192
|
+
output_lines.append(f"Already up to date: {len(unchanged_files)} files")
|
|
193
|
+
output_lines.append("")
|
|
194
|
+
|
|
195
|
+
# Perform sync if not dry run
|
|
196
|
+
if not dry_run:
|
|
197
|
+
synced = 0
|
|
198
|
+
skipped = 0
|
|
199
|
+
|
|
200
|
+
# Sync new files
|
|
201
|
+
for rel_path in new_files:
|
|
202
|
+
src = repo_npc_team / rel_path
|
|
203
|
+
dst = local_npc_team / rel_path
|
|
204
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
shutil.copy2(src, dst)
|
|
206
|
+
synced += 1
|
|
207
|
+
|
|
208
|
+
# Sync updated files
|
|
209
|
+
for rel_path in updated_files:
|
|
210
|
+
src = repo_npc_team / rel_path
|
|
211
|
+
dst = local_npc_team / rel_path
|
|
212
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
shutil.copy2(src, dst)
|
|
214
|
+
synced += 1
|
|
215
|
+
|
|
216
|
+
# Handle locally modified files
|
|
217
|
+
for rel_path, _, _ in modified_locally:
|
|
218
|
+
if force:
|
|
219
|
+
src = repo_npc_team / rel_path
|
|
220
|
+
dst = local_npc_team / rel_path
|
|
221
|
+
shutil.copy2(src, dst)
|
|
222
|
+
synced += 1
|
|
223
|
+
else:
|
|
224
|
+
skipped += 1
|
|
225
|
+
|
|
226
|
+
output_lines.append(f"Synced: {synced} files")
|
|
227
|
+
if skipped:
|
|
228
|
+
output_lines.append(f"Skipped: {skipped} locally modified files")
|
|
229
|
+
|
|
230
|
+
context['output'] = "\n".join(output_lines)
|