npcsh 1.1.13__py3-none-any.whl → 1.1.15__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 +491 -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.13.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.13.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/edit_file.jinx +3 -3
- {npcsh-1.1.13.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.13.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.15.data/data/npcsh/npc_team/alicanto.npc +23 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/arxiv.jinx +76 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/browser_action.jinx +220 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/browser_screenshot.jinx +40 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/build.jinx +8 -8
- npcsh-1.1.15.data/data/npcsh/npc_team/click.jinx +23 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/close_browser.jinx +14 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/convene.jinx +232 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/corca.npc +31 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/delegate.jinx +184 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.15.data/data/npcsh/npc_team}/edit_file.jinx +3 -3
- npcsh-1.1.15.data/data/npcsh/npc_team/frederic.npc +27 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/guac.npc +22 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/jinxs.jinx +176 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/kadiefa.npc +21 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/key_press.jinx +26 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/launch_app.jinx +37 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.15.data/data/npcsh/npc_team}/load_file.jinx +1 -1
- npcsh-1.1.15.data/data/npcsh/npc_team/nql.jinx +141 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/open_browser.jinx +43 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/paper_search.jinx +101 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/paste.jinx +134 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/plonk.npc +27 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/plonkjr.npc +23 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/screenshot.jinx +23 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.15.data/data/npcsh/npc_team}/search.jinx +2 -1
- npcsh-1.1.15.data/data/npcsh/npc_team/semantic_scholar.jinx +69 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sh.jinx +2 -8
- npcsh-1.1.15.data/data/npcsh/npc_team/shh.jinx +17 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/sibiji.npc +24 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sql.jinx +1 -1
- npcsh-1.1.15.data/data/npcsh/npc_team/switch.jinx +62 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/switches.jinx +61 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/sync.jinx +230 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/teamviz.jinx +205 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/type_text.jinx +27 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/verbose.jinx +17 -0
- {npcsh/npc_team/jinxs/utils → npcsh-1.1.15.data/data/npcsh/npc_team}/vixynt.jinx +8 -30
- npcsh-1.1.15.data/data/npcsh/npc_team/wait.jinx +21 -0
- npcsh-1.1.15.data/data/npcsh/npc_team/wander.jinx +152 -0
- {npcsh-1.1.13.dist-info → npcsh-1.1.15.dist-info}/METADATA +399 -58
- npcsh-1.1.15.dist-info/RECORD +170 -0
- npcsh-1.1.15.dist-info/entry_points.txt +19 -0
- npcsh-1.1.15.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.13.data/data/npcsh/npc_team/agent.jinx +0 -17
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.jinx +0 -194
- npcsh-1.1.13.data/data/npcsh/npc_team/alicanto.npc +0 -2
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.jinx +0 -249
- npcsh-1.1.13.data/data/npcsh/npc_team/corca.npc +0 -12
- npcsh-1.1.13.data/data/npcsh/npc_team/foreman.npc +0 -7
- npcsh-1.1.13.data/data/npcsh/npc_team/frederic.npc +0 -6
- npcsh-1.1.13.data/data/npcsh/npc_team/guac.jinx +0 -317
- npcsh-1.1.13.data/data/npcsh/npc_team/jinxs.jinx +0 -32
- npcsh-1.1.13.data/data/npcsh/npc_team/kadiefa.npc +0 -3
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.jinx +0 -214
- npcsh-1.1.13.data/data/npcsh/npc_team/plonk.npc +0 -2
- npcsh-1.1.13.data/data/npcsh/npc_team/plonkjr.npc +0 -2
- npcsh-1.1.13.data/data/npcsh/npc_team/pti.jinx +0 -170
- npcsh-1.1.13.data/data/npcsh/npc_team/sibiji.npc +0 -3
- npcsh-1.1.13.data/data/npcsh/npc_team/wander.jinx +0 -186
- npcsh-1.1.13.dist-info/RECORD +0 -135
- npcsh-1.1.13.dist-info/entry_points.txt +0 -9
- npcsh-1.1.13.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.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/yap.jinx +0 -0
- {npcsh-1.1.13.data → npcsh-1.1.15.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.13.dist-info → npcsh-1.1.15.dist-info}/WHEEL +0 -0
- {npcsh-1.1.13.dist-info → npcsh-1.1.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
jinx_name: delegate
|
|
2
|
+
description: Delegate a task to another NPC with review and feedback loop until completion. Choose the NPC whose directive best matches the task.
|
|
3
|
+
inputs:
|
|
4
|
+
- npc_name:
|
|
5
|
+
description: "Name of the NPC to delegate to"
|
|
6
|
+
- task:
|
|
7
|
+
description: "The task or request to delegate to the NPC"
|
|
8
|
+
- max_iterations: "10"
|
|
9
|
+
steps:
|
|
10
|
+
- name: delegate_with_review
|
|
11
|
+
engine: python
|
|
12
|
+
code: |
|
|
13
|
+
from termcolor import colored
|
|
14
|
+
from npcpy.llm_funcs import get_llm_response
|
|
15
|
+
|
|
16
|
+
# Try to get spinner for status updates
|
|
17
|
+
try:
|
|
18
|
+
from npcsh.ui import get_current_spinner
|
|
19
|
+
spinner = get_current_spinner()
|
|
20
|
+
except:
|
|
21
|
+
spinner = None
|
|
22
|
+
|
|
23
|
+
target_name = {{ npc_name | default("") | tojson }}.lower().strip()
|
|
24
|
+
task_request = {{ task | default("") | tojson }}
|
|
25
|
+
max_iters = int({{ max_iterations | default("10") | tojson }} or "10")
|
|
26
|
+
|
|
27
|
+
team_obj = context.get('team') or getattr(npc, 'team', None)
|
|
28
|
+
orchestrator = context.get('npc') or npc
|
|
29
|
+
orchestrator_name = getattr(orchestrator, 'name', 'orchestrator')
|
|
30
|
+
|
|
31
|
+
if not team_obj:
|
|
32
|
+
output = "Error: No team available for delegation"
|
|
33
|
+
exit()
|
|
34
|
+
|
|
35
|
+
if not hasattr(team_obj, 'npcs') or target_name not in team_obj.npcs:
|
|
36
|
+
available = list(team_obj.npcs.keys()) if hasattr(team_obj, 'npcs') else []
|
|
37
|
+
output = "Error: NPC '{}' not found. Available: {}".format(target_name, ', '.join(available))
|
|
38
|
+
exit()
|
|
39
|
+
|
|
40
|
+
target_npc = team_obj.npcs[target_name]
|
|
41
|
+
target_jinxs = dict((k, v) for k, v in target_npc.jinxs_dict.items() if k != 'delegate')
|
|
42
|
+
|
|
43
|
+
sep = '-' * 60
|
|
44
|
+
print(colored("\n" + sep, "cyan"))
|
|
45
|
+
print(colored(" Delegating to @" + target_name, "yellow", attrs=["bold"]))
|
|
46
|
+
task_preview = task_request[:100] + ('...' if len(task_request) > 100 else '')
|
|
47
|
+
print(colored(" Task: " + task_preview, "white", attrs=["dark"]))
|
|
48
|
+
print(colored(sep + "\n", "cyan"))
|
|
49
|
+
print(colored(" [{}] Model: {}".format(target_name, target_npc.model), "white", attrs=["dark"]))
|
|
50
|
+
jinx_list = ', '.join(list(target_jinxs.keys())[:8])
|
|
51
|
+
print(colored(" [{}] Jinxs: {}...".format(target_name, jinx_list), "white", attrs=["dark"]))
|
|
52
|
+
|
|
53
|
+
# Update spinner to show sub-agent
|
|
54
|
+
if spinner:
|
|
55
|
+
spinner.set_message("{} delegated to {}".format(orchestrator_name, target_name))
|
|
56
|
+
|
|
57
|
+
current_task = task_request
|
|
58
|
+
iteration = 0
|
|
59
|
+
final_output = None
|
|
60
|
+
task_complete = False
|
|
61
|
+
|
|
62
|
+
while iteration < max_iters and not task_complete:
|
|
63
|
+
iteration += 1
|
|
64
|
+
|
|
65
|
+
# Update spinner with current iteration
|
|
66
|
+
if spinner:
|
|
67
|
+
spinner.set_message("{} working (iter {}/{})".format(target_name, iteration, max_iters))
|
|
68
|
+
|
|
69
|
+
if iteration > 1:
|
|
70
|
+
print(colored("\n" + sep, "yellow"))
|
|
71
|
+
iter_msg = " Iteration {}/{} - Re-tasking @{}".format(iteration, max_iters, target_name)
|
|
72
|
+
print(colored(iter_msg, "yellow", attrs=["bold"]))
|
|
73
|
+
print(colored(sep + "\n", "yellow"))
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
result = target_npc.check_llm_command(
|
|
77
|
+
current_task,
|
|
78
|
+
context=context,
|
|
79
|
+
team=team_obj,
|
|
80
|
+
jinxs=target_jinxs,
|
|
81
|
+
stream=False,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if isinstance(result, dict):
|
|
85
|
+
delegate_output = result.get('output') or result.get('response') or str(result)
|
|
86
|
+
delegate_messages = result.get('messages', [])
|
|
87
|
+
else:
|
|
88
|
+
delegate_output = str(result)
|
|
89
|
+
delegate_messages = []
|
|
90
|
+
|
|
91
|
+
print(colored("\n" + sep, "cyan"))
|
|
92
|
+
print(colored(" @{} iteration {} complete".format(target_name, iteration), "green"))
|
|
93
|
+
print(colored(sep + "\n", "cyan"))
|
|
94
|
+
|
|
95
|
+
# Update spinner for review phase
|
|
96
|
+
if spinner:
|
|
97
|
+
spinner.set_message("{} reviewing {}'s work".format(orchestrator_name, target_name))
|
|
98
|
+
|
|
99
|
+
# Build review prompt without f-strings to avoid YAML issues
|
|
100
|
+
output_preview = delegate_output[:2000] if delegate_output else 'No output received'
|
|
101
|
+
msg_preview = str(delegate_messages[-5:]) if delegate_messages else 'No messages'
|
|
102
|
+
|
|
103
|
+
review_lines = [
|
|
104
|
+
"You are reviewing work done by @{} on this task:".format(target_name),
|
|
105
|
+
"",
|
|
106
|
+
"ORIGINAL TASK: " + task_request,
|
|
107
|
+
"",
|
|
108
|
+
"ITERATION: {}/{}".format(iteration, max_iters),
|
|
109
|
+
"",
|
|
110
|
+
"SUB-AGENT OUTPUT:",
|
|
111
|
+
output_preview,
|
|
112
|
+
"",
|
|
113
|
+
"RECENT MESSAGES:",
|
|
114
|
+
msg_preview,
|
|
115
|
+
"",
|
|
116
|
+
"Evaluate if the task is complete. Consider:",
|
|
117
|
+
"1. Did the sub-agent accomplish what was asked?",
|
|
118
|
+
"2. Are there obvious errors or incomplete steps?",
|
|
119
|
+
"3. For GUI tasks: Did they fill in all required fields?",
|
|
120
|
+
"",
|
|
121
|
+
"Respond EXACTLY like this:",
|
|
122
|
+
"COMPLETE: YES or NO",
|
|
123
|
+
"FEEDBACK: If NO, what should be done next",
|
|
124
|
+
"SUMMARY: Brief summary of progress"
|
|
125
|
+
]
|
|
126
|
+
review_prompt = "\n".join(review_lines)
|
|
127
|
+
|
|
128
|
+
review_result = get_llm_response(
|
|
129
|
+
review_prompt,
|
|
130
|
+
model=getattr(orchestrator, 'model', 'gemini-2.5-flash'),
|
|
131
|
+
provider=getattr(orchestrator, 'provider', 'gemini'),
|
|
132
|
+
npc=orchestrator,
|
|
133
|
+
temperature=0.3
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
review_text = str(review_result.get('response', ''))
|
|
137
|
+
is_complete = 'COMPLETE: YES' in review_text.upper()
|
|
138
|
+
|
|
139
|
+
feedback = ""
|
|
140
|
+
if 'FEEDBACK:' in review_text:
|
|
141
|
+
fb_start = review_text.find('FEEDBACK:') + 9
|
|
142
|
+
fb_end = review_text.find('SUMMARY:', fb_start) if 'SUMMARY:' in review_text else len(review_text)
|
|
143
|
+
feedback = review_text[fb_start:fb_end].strip()
|
|
144
|
+
|
|
145
|
+
summary = ""
|
|
146
|
+
if 'SUMMARY:' in review_text:
|
|
147
|
+
summary = review_text[review_text.find('SUMMARY:') + 8:].strip()
|
|
148
|
+
|
|
149
|
+
if is_complete:
|
|
150
|
+
task_complete = True
|
|
151
|
+
print(colored("\n Task completed successfully", "green", attrs=["bold"]))
|
|
152
|
+
if summary:
|
|
153
|
+
print(colored(" Summary: " + summary[:200], "white", attrs=["dark"]))
|
|
154
|
+
final_output = "[{}] Task completed.\n{}".format(target_name, summary)
|
|
155
|
+
else:
|
|
156
|
+
print(colored("\n Task incomplete - providing feedback", "yellow"))
|
|
157
|
+
if feedback:
|
|
158
|
+
print(colored(" Feedback: " + feedback[:200] + "...", "white", attrs=["dark"]))
|
|
159
|
+
|
|
160
|
+
followup_lines = [
|
|
161
|
+
"Continue the previous task. Feedback from orchestrator:",
|
|
162
|
+
"",
|
|
163
|
+
"ORIGINAL TASK: " + task_request,
|
|
164
|
+
"",
|
|
165
|
+
"FEEDBACK: " + feedback,
|
|
166
|
+
"",
|
|
167
|
+
"Continue and complete the task based on this feedback."
|
|
168
|
+
]
|
|
169
|
+
current_task = "\n".join(followup_lines)
|
|
170
|
+
|
|
171
|
+
if delegate_messages:
|
|
172
|
+
context['messages'] = delegate_messages
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(colored(" Error in iteration {}: {}".format(iteration, e), "red"))
|
|
176
|
+
final_output = "Error delegating to {}: {}".format(target_name, str(e))
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
if not task_complete and iteration >= max_iters:
|
|
180
|
+
print(colored("\n Max iterations ({}) reached".format(max_iters), "yellow"))
|
|
181
|
+
status = summary if summary else 'Unknown'
|
|
182
|
+
final_output = "[{}] Task incomplete after {} iterations. Status: {}".format(target_name, max_iters, status)
|
|
183
|
+
|
|
184
|
+
output = final_output or "[{}]: No output received".format(target_name)
|
|
@@ -13,9 +13,9 @@ steps:
|
|
|
13
13
|
from npcpy.llm_funcs import get_llm_response
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
file_path = os.path.expanduser(
|
|
17
|
-
edit_instructions =
|
|
18
|
-
backup_str =
|
|
16
|
+
file_path = os.path.expanduser({{ file_path | tojson }})
|
|
17
|
+
edit_instructions = {{ edit_instructions | string | tojson }}
|
|
18
|
+
backup_str = {{ backup | default("true") | string | tojson }}
|
|
19
19
|
create_backup = backup_str.lower() not in ('false', 'no', '0', '')
|
|
20
20
|
|
|
21
21
|
|
|
@@ -0,0 +1,27 @@
|
|
|
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"
|
|
13
|
+
primary_directive: |
|
|
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
|
|
@@ -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,176 @@
|
|
|
1
|
+
jinx_name: jinxs
|
|
2
|
+
description: "Show available jinxs organized by folder. Use /jinxs <path> for details on a specific folder."
|
|
3
|
+
inputs:
|
|
4
|
+
- path: "" # Optional path to show details for (e.g., "lib/core", "bin")
|
|
5
|
+
steps:
|
|
6
|
+
- name: list_jinxs
|
|
7
|
+
engine: python
|
|
8
|
+
code: |
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
filter_path = context.get('path', '').strip()
|
|
14
|
+
|
|
15
|
+
# Find jinxs directory from team or fallback
|
|
16
|
+
jinxs_dir = None
|
|
17
|
+
if hasattr(npc, 'team') and npc.team:
|
|
18
|
+
if hasattr(npc.team, 'jinxs_dir') and npc.team.jinxs_dir:
|
|
19
|
+
jinxs_dir = Path(npc.team.jinxs_dir)
|
|
20
|
+
elif hasattr(npc.team, 'team_path') and npc.team.team_path:
|
|
21
|
+
candidate = Path(npc.team.team_path) / "jinxs"
|
|
22
|
+
if candidate.exists():
|
|
23
|
+
jinxs_dir = candidate
|
|
24
|
+
|
|
25
|
+
if not jinxs_dir:
|
|
26
|
+
# Fallback to global jinxs
|
|
27
|
+
global_jinxs = Path.home() / ".npcsh" / "npc_team" / "jinxs"
|
|
28
|
+
if global_jinxs.exists():
|
|
29
|
+
jinxs_dir = global_jinxs
|
|
30
|
+
|
|
31
|
+
if not jinxs_dir or not jinxs_dir.exists():
|
|
32
|
+
output = "Error: Could not find jinxs directory"
|
|
33
|
+
exit()
|
|
34
|
+
|
|
35
|
+
def get_jinx_info(jinx_path):
|
|
36
|
+
"""Extract name and description from a jinx file."""
|
|
37
|
+
try:
|
|
38
|
+
with open(jinx_path, 'r') as f:
|
|
39
|
+
content = f.read()
|
|
40
|
+
# Parse just the header (before steps:)
|
|
41
|
+
header = content.split('steps:')[0] if 'steps:' in content else content
|
|
42
|
+
data = yaml.safe_load(header)
|
|
43
|
+
name = data.get('jinx_name', jinx_path.stem)
|
|
44
|
+
desc = data.get('description', 'No description')
|
|
45
|
+
return name, desc
|
|
46
|
+
except:
|
|
47
|
+
return jinx_path.stem, 'No description'
|
|
48
|
+
|
|
49
|
+
def get_folder_structure(base_path):
|
|
50
|
+
"""Get jinxs organized by folder."""
|
|
51
|
+
structure = {}
|
|
52
|
+
for root, dirs, files in os.walk(base_path):
|
|
53
|
+
# Skip hidden directories
|
|
54
|
+
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
55
|
+
|
|
56
|
+
jinx_files = [f for f in files if f.endswith('.jinx')]
|
|
57
|
+
if jinx_files:
|
|
58
|
+
rel_path = Path(root).relative_to(base_path)
|
|
59
|
+
rel_str = str(rel_path) if str(rel_path) != '.' else 'root'
|
|
60
|
+
structure[rel_str] = []
|
|
61
|
+
for jf in sorted(jinx_files):
|
|
62
|
+
jinx_path = Path(root) / jf
|
|
63
|
+
name, desc = get_jinx_info(jinx_path)
|
|
64
|
+
structure[rel_str].append((name, desc, jf))
|
|
65
|
+
return structure
|
|
66
|
+
|
|
67
|
+
output_lines = []
|
|
68
|
+
|
|
69
|
+
if filter_path:
|
|
70
|
+
# Show details for a specific path
|
|
71
|
+
target_path = jinxs_dir / filter_path
|
|
72
|
+
if not target_path.exists():
|
|
73
|
+
# Try to find a matching folder
|
|
74
|
+
matches = []
|
|
75
|
+
for root, dirs, files in os.walk(jinxs_dir):
|
|
76
|
+
rel = Path(root).relative_to(jinxs_dir)
|
|
77
|
+
if filter_path in str(rel) or filter_path in Path(root).name:
|
|
78
|
+
matches.append(rel)
|
|
79
|
+
|
|
80
|
+
if matches:
|
|
81
|
+
output_lines.append(f"No exact match for '{filter_path}'. Did you mean:\n")
|
|
82
|
+
for m in matches[:5]:
|
|
83
|
+
output_lines.append(f" /jinxs {m}\n")
|
|
84
|
+
output = "".join(output_lines)
|
|
85
|
+
exit()
|
|
86
|
+
else:
|
|
87
|
+
output = f"No jinxs found at path: {filter_path}"
|
|
88
|
+
exit()
|
|
89
|
+
|
|
90
|
+
# Get jinxs in this path
|
|
91
|
+
structure = get_folder_structure(target_path)
|
|
92
|
+
if not structure:
|
|
93
|
+
# Check if it's a single folder with jinxs
|
|
94
|
+
jinx_files = list(target_path.glob("*.jinx"))
|
|
95
|
+
if jinx_files:
|
|
96
|
+
output_lines.append(f"Jinxs in {filter_path}:\n\n")
|
|
97
|
+
for jf in sorted(jinx_files):
|
|
98
|
+
name, desc = get_jinx_info(jf)
|
|
99
|
+
output_lines.append(f" /{name}\n")
|
|
100
|
+
output_lines.append(f" {desc}\n\n")
|
|
101
|
+
else:
|
|
102
|
+
output = f"No jinxs found at path: {filter_path}"
|
|
103
|
+
exit()
|
|
104
|
+
else:
|
|
105
|
+
output_lines.append(f"Jinxs in {filter_path}:\n\n")
|
|
106
|
+
for folder, jinxs in sorted(structure.items()):
|
|
107
|
+
if folder != 'root':
|
|
108
|
+
output_lines.append(f" {folder}/\n")
|
|
109
|
+
for name, desc, filename in jinxs:
|
|
110
|
+
prefix = " " if folder != 'root' else " "
|
|
111
|
+
output_lines.append(f"{prefix}/{name} - {desc}\n")
|
|
112
|
+
output_lines.append("\n")
|
|
113
|
+
|
|
114
|
+
else:
|
|
115
|
+
# Show overview organized by folder
|
|
116
|
+
structure = get_folder_structure(jinxs_dir)
|
|
117
|
+
|
|
118
|
+
output_lines.append("Available Jinxs\n")
|
|
119
|
+
output_lines.append("=" * 40 + "\n\n")
|
|
120
|
+
|
|
121
|
+
# Group by top-level folder
|
|
122
|
+
top_level = {}
|
|
123
|
+
for folder, jinxs in structure.items():
|
|
124
|
+
if folder == 'root':
|
|
125
|
+
top = 'root'
|
|
126
|
+
else:
|
|
127
|
+
top = folder.split('/')[0] if '/' in folder else folder
|
|
128
|
+
|
|
129
|
+
if top not in top_level:
|
|
130
|
+
top_level[top] = {'subfolders': {}, 'jinxs': []}
|
|
131
|
+
|
|
132
|
+
if folder == top or folder == 'root':
|
|
133
|
+
top_level[top]['jinxs'].extend(jinxs)
|
|
134
|
+
else:
|
|
135
|
+
subfolder = '/'.join(folder.split('/')[1:])
|
|
136
|
+
if subfolder not in top_level[top]['subfolders']:
|
|
137
|
+
top_level[top]['subfolders'][subfolder] = []
|
|
138
|
+
top_level[top]['subfolders'][subfolder].extend(jinxs)
|
|
139
|
+
|
|
140
|
+
# Display
|
|
141
|
+
folder_order = ['bin', 'lib', 'npc_studio', 'root']
|
|
142
|
+
sorted_folders = sorted(top_level.keys(), key=lambda x: (folder_order.index(x) if x in folder_order else 99, x))
|
|
143
|
+
|
|
144
|
+
for top in sorted_folders:
|
|
145
|
+
data = top_level[top]
|
|
146
|
+
|
|
147
|
+
if top == 'root':
|
|
148
|
+
if data['jinxs']:
|
|
149
|
+
output_lines.append("Root Jinxs:\n")
|
|
150
|
+
for name, desc, _ in data['jinxs']:
|
|
151
|
+
output_lines.append(f" /{name} - {desc}\n")
|
|
152
|
+
output_lines.append("\n")
|
|
153
|
+
else:
|
|
154
|
+
total = len(data['jinxs'])
|
|
155
|
+
for sf_jinxs in data['subfolders'].values():
|
|
156
|
+
total += len(sf_jinxs)
|
|
157
|
+
|
|
158
|
+
output_lines.append(f"{top}/ ({total} jinxs)\n")
|
|
159
|
+
|
|
160
|
+
# Show direct jinxs
|
|
161
|
+
if data['jinxs']:
|
|
162
|
+
for name, desc, _ in data['jinxs'][:3]:
|
|
163
|
+
output_lines.append(f" /{name} - {desc}\n")
|
|
164
|
+
if len(data['jinxs']) > 3:
|
|
165
|
+
output_lines.append(f" ... and {len(data['jinxs']) - 3} more\n")
|
|
166
|
+
|
|
167
|
+
# Show subfolders summary
|
|
168
|
+
if data['subfolders']:
|
|
169
|
+
for subfolder, jinxs in sorted(data['subfolders'].items()):
|
|
170
|
+
output_lines.append(f" {subfolder}/ ({len(jinxs)} jinxs)\n")
|
|
171
|
+
|
|
172
|
+
output_lines.append(f" → /jinxs {top} for details\n\n")
|
|
173
|
+
|
|
174
|
+
output_lines.append("Use /jinxs <path> for details (e.g., /jinxs lib/core)\n")
|
|
175
|
+
|
|
176
|
+
output = "".join(output_lines)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: kadiefa
|
|
2
|
+
ascii_art: |
|
|
3
|
+
🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️
|
|
4
|
+
██ ██ █████ ██████ ██ ███████ ███████ █████
|
|
5
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
6
|
+
█████ ███████ ██ ██ ██ █████ █████ ███████ 🐆
|
|
7
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
8
|
+
██ ██ ██ ██ ██████ ██ ███████ ██ ██ ██
|
|
9
|
+
🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️🏔️
|
|
10
|
+
colors:
|
|
11
|
+
top: "255,255,255"
|
|
12
|
+
bottom: "173,216,230"
|
|
13
|
+
primary_directive: |
|
|
14
|
+
You are kadiefa, the exploratory snow leopard. You love to find new paths and to explore hidden gems.
|
|
15
|
+
You go into caverns no cat has ventured into before. You climb peaks that others call crazy.
|
|
16
|
+
Your role is to lead users on wandering explorations - deep research journeys that follow threads
|
|
17
|
+
wherever they lead. You help users explore complex research questions and think outside the box.
|
|
18
|
+
Use web search and browsing tools to discover new information.
|
|
19
|
+
jinxs:
|
|
20
|
+
- lib/core/python
|
|
21
|
+
- bin/wander
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
jinx_name: key_press
|
|
2
|
+
description: |
|
|
3
|
+
Press a keyboard key or key combination.
|
|
4
|
+
Valid keys: enter, tab, escape, backspace, delete, space, up, down, left, right,
|
|
5
|
+
home, end, pageup, pagedown, f1-f12, ctrl, alt, shift, command.
|
|
6
|
+
For combinations use + like: ctrl+a, ctrl+c, ctrl+v, alt+tab, ctrl+shift+t
|
|
7
|
+
For regular letters/numbers, use type_text instead.
|
|
8
|
+
inputs:
|
|
9
|
+
- key: "enter"
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: perform_key
|
|
13
|
+
engine: python
|
|
14
|
+
code: |
|
|
15
|
+
from npcpy.work.desktop import perform_action
|
|
16
|
+
|
|
17
|
+
key = context.get('key', 'enter')
|
|
18
|
+
messages = context.get('messages', [])
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
perform_action({'type': 'key', 'keys': key})
|
|
22
|
+
context['output'] = "Pressed key: " + str(key)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
context['output'] = "Key press failed: " + str(e)
|
|
25
|
+
|
|
26
|
+
context['messages'] = messages
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
jinx_name: launch_app
|
|
2
|
+
description: Launch an application on the system
|
|
3
|
+
inputs:
|
|
4
|
+
- command: "" # Command to launch (e.g., "open -a Firefox" on macOS)
|
|
5
|
+
|
|
6
|
+
steps:
|
|
7
|
+
- name: perform_launch
|
|
8
|
+
engine: python
|
|
9
|
+
code: |
|
|
10
|
+
import platform
|
|
11
|
+
from npcpy.work.desktop import perform_action
|
|
12
|
+
|
|
13
|
+
command = context.get('command', '')
|
|
14
|
+
messages = context.get('messages', [])
|
|
15
|
+
|
|
16
|
+
if not command:
|
|
17
|
+
system = platform.system()
|
|
18
|
+
if system == "Darwin":
|
|
19
|
+
examples = "open -a Firefox, open -a TextEdit"
|
|
20
|
+
elif system == "Windows":
|
|
21
|
+
examples = "start firefox, notepad, calc"
|
|
22
|
+
else:
|
|
23
|
+
examples = "firefox &, gedit &"
|
|
24
|
+
context['output'] = f"Usage: /launch_app <command>\nExamples: {examples}"
|
|
25
|
+
context['messages'] = messages
|
|
26
|
+
exit()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
result = perform_action({'type': 'bash', 'command': command})
|
|
30
|
+
if result.get('status') == 'success':
|
|
31
|
+
context['output'] = f"Launched: {command}"
|
|
32
|
+
else:
|
|
33
|
+
context['output'] = f"Launch failed: {result.get('message', 'unknown error')}"
|
|
34
|
+
except Exception as e:
|
|
35
|
+
context['output'] = f"Launch failed: {e}"
|
|
36
|
+
|
|
37
|
+
context['messages'] = messages
|
|
@@ -10,7 +10,7 @@ steps:
|
|
|
10
10
|
from npcpy.data.load import load_file_contents
|
|
11
11
|
|
|
12
12
|
# Expand user path and get absolute path
|
|
13
|
-
file_path = os.path.expanduser(
|
|
13
|
+
file_path = os.path.expanduser({{ file_path | tojson }})
|
|
14
14
|
|
|
15
15
|
# Check if file exists
|
|
16
16
|
if not os.path.exists(file_path):
|
|
@@ -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()
|