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.
Files changed (168) hide show
  1. npcsh/_state.py +533 -80
  2. npcsh/mcp_server.py +2 -1
  3. npcsh/npc.py +84 -32
  4. npcsh/npc_team/alicanto.npc +22 -1
  5. npcsh/npc_team/corca.npc +28 -9
  6. npcsh/npc_team/frederic.npc +25 -4
  7. npcsh/npc_team/guac.npc +22 -0
  8. npcsh/npc_team/jinxs/bin/nql.jinx +141 -0
  9. npcsh/npc_team/jinxs/bin/sync.jinx +230 -0
  10. {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/bin}/vixynt.jinx +8 -30
  11. npcsh/npc_team/jinxs/bin/wander.jinx +152 -0
  12. npcsh/npc_team/jinxs/lib/browser/browser_action.jinx +220 -0
  13. npcsh/npc_team/jinxs/lib/browser/browser_screenshot.jinx +40 -0
  14. npcsh/npc_team/jinxs/lib/browser/close_browser.jinx +14 -0
  15. npcsh/npc_team/jinxs/lib/browser/open_browser.jinx +43 -0
  16. npcsh/npc_team/jinxs/lib/computer_use/click.jinx +23 -0
  17. npcsh/npc_team/jinxs/lib/computer_use/key_press.jinx +26 -0
  18. npcsh/npc_team/jinxs/lib/computer_use/launch_app.jinx +37 -0
  19. npcsh/npc_team/jinxs/lib/computer_use/screenshot.jinx +23 -0
  20. npcsh/npc_team/jinxs/lib/computer_use/type_text.jinx +27 -0
  21. npcsh/npc_team/jinxs/lib/computer_use/wait.jinx +21 -0
  22. {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/edit_file.jinx +3 -3
  23. {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/load_file.jinx +1 -1
  24. npcsh/npc_team/jinxs/lib/core/paste.jinx +134 -0
  25. {npcsh-1.1.14.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/lib/core}/search.jinx +2 -1
  26. npcsh/npc_team/jinxs/{code → lib/core}/sh.jinx +2 -8
  27. npcsh/npc_team/jinxs/{code → lib/core}/sql.jinx +1 -1
  28. npcsh/npc_team/jinxs/lib/orchestration/convene.jinx +232 -0
  29. npcsh/npc_team/jinxs/lib/orchestration/delegate.jinx +184 -0
  30. npcsh/npc_team/jinxs/lib/research/arxiv.jinx +76 -0
  31. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +101 -0
  32. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +69 -0
  33. npcsh/npc_team/jinxs/{utils/core → lib/utils}/build.jinx +8 -8
  34. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +176 -0
  35. npcsh/npc_team/jinxs/lib/utils/shh.jinx +17 -0
  36. npcsh/npc_team/jinxs/lib/utils/switch.jinx +62 -0
  37. npcsh/npc_team/jinxs/lib/utils/switches.jinx +61 -0
  38. npcsh/npc_team/jinxs/lib/utils/teamviz.jinx +205 -0
  39. npcsh/npc_team/jinxs/lib/utils/verbose.jinx +17 -0
  40. npcsh/npc_team/kadiefa.npc +19 -1
  41. npcsh/npc_team/plonk.npc +26 -1
  42. npcsh/npc_team/plonkjr.npc +22 -1
  43. npcsh/npc_team/sibiji.npc +23 -2
  44. npcsh/npcsh.py +153 -39
  45. npcsh/ui.py +22 -1
  46. npcsh-1.1.16.data/data/npcsh/npc_team/alicanto.npc +23 -0
  47. npcsh-1.1.16.data/data/npcsh/npc_team/arxiv.jinx +76 -0
  48. npcsh-1.1.16.data/data/npcsh/npc_team/browser_action.jinx +220 -0
  49. npcsh-1.1.16.data/data/npcsh/npc_team/browser_screenshot.jinx +40 -0
  50. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/build.jinx +8 -8
  51. npcsh-1.1.16.data/data/npcsh/npc_team/click.jinx +23 -0
  52. npcsh-1.1.16.data/data/npcsh/npc_team/close_browser.jinx +14 -0
  53. npcsh-1.1.16.data/data/npcsh/npc_team/convene.jinx +232 -0
  54. npcsh-1.1.16.data/data/npcsh/npc_team/corca.npc +31 -0
  55. npcsh-1.1.16.data/data/npcsh/npc_team/delegate.jinx +184 -0
  56. {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/edit_file.jinx +3 -3
  57. npcsh-1.1.16.data/data/npcsh/npc_team/frederic.npc +27 -0
  58. npcsh-1.1.16.data/data/npcsh/npc_team/guac.npc +22 -0
  59. npcsh-1.1.16.data/data/npcsh/npc_team/jinxs.jinx +176 -0
  60. npcsh-1.1.16.data/data/npcsh/npc_team/kadiefa.npc +21 -0
  61. npcsh-1.1.16.data/data/npcsh/npc_team/key_press.jinx +26 -0
  62. npcsh-1.1.16.data/data/npcsh/npc_team/launch_app.jinx +37 -0
  63. {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/load_file.jinx +1 -1
  64. npcsh-1.1.16.data/data/npcsh/npc_team/nql.jinx +141 -0
  65. npcsh-1.1.16.data/data/npcsh/npc_team/open_browser.jinx +43 -0
  66. npcsh-1.1.16.data/data/npcsh/npc_team/paper_search.jinx +101 -0
  67. npcsh-1.1.16.data/data/npcsh/npc_team/paste.jinx +134 -0
  68. npcsh-1.1.16.data/data/npcsh/npc_team/plonk.npc +27 -0
  69. npcsh-1.1.16.data/data/npcsh/npc_team/plonkjr.npc +23 -0
  70. npcsh-1.1.16.data/data/npcsh/npc_team/screenshot.jinx +23 -0
  71. {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/search.jinx +2 -1
  72. npcsh-1.1.16.data/data/npcsh/npc_team/semantic_scholar.jinx +69 -0
  73. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sh.jinx +2 -8
  74. npcsh-1.1.16.data/data/npcsh/npc_team/shh.jinx +17 -0
  75. npcsh-1.1.16.data/data/npcsh/npc_team/sibiji.npc +24 -0
  76. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sql.jinx +1 -1
  77. npcsh-1.1.16.data/data/npcsh/npc_team/switch.jinx +62 -0
  78. npcsh-1.1.16.data/data/npcsh/npc_team/switches.jinx +61 -0
  79. npcsh-1.1.16.data/data/npcsh/npc_team/sync.jinx +230 -0
  80. npcsh-1.1.16.data/data/npcsh/npc_team/teamviz.jinx +205 -0
  81. npcsh-1.1.16.data/data/npcsh/npc_team/type_text.jinx +27 -0
  82. npcsh-1.1.16.data/data/npcsh/npc_team/verbose.jinx +17 -0
  83. {npcsh/npc_team/jinxs/utils → npcsh-1.1.16.data/data/npcsh/npc_team}/vixynt.jinx +8 -30
  84. npcsh-1.1.16.data/data/npcsh/npc_team/wait.jinx +21 -0
  85. npcsh-1.1.16.data/data/npcsh/npc_team/wander.jinx +152 -0
  86. {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/METADATA +399 -58
  87. npcsh-1.1.16.dist-info/RECORD +170 -0
  88. npcsh-1.1.16.dist-info/entry_points.txt +19 -0
  89. npcsh-1.1.16.dist-info/top_level.txt +2 -0
  90. project/__init__.py +1 -0
  91. npcsh/npc_team/foreman.npc +0 -7
  92. npcsh/npc_team/jinxs/modes/alicanto.jinx +0 -194
  93. npcsh/npc_team/jinxs/modes/corca.jinx +0 -249
  94. npcsh/npc_team/jinxs/modes/guac.jinx +0 -317
  95. npcsh/npc_team/jinxs/modes/plonk.jinx +0 -214
  96. npcsh/npc_team/jinxs/modes/pti.jinx +0 -170
  97. npcsh/npc_team/jinxs/modes/wander.jinx +0 -186
  98. npcsh/npc_team/jinxs/utils/agent.jinx +0 -17
  99. npcsh/npc_team/jinxs/utils/core/jinxs.jinx +0 -32
  100. npcsh-1.1.14.data/data/npcsh/npc_team/agent.jinx +0 -17
  101. npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.jinx +0 -194
  102. npcsh-1.1.14.data/data/npcsh/npc_team/alicanto.npc +0 -2
  103. npcsh-1.1.14.data/data/npcsh/npc_team/corca.jinx +0 -249
  104. npcsh-1.1.14.data/data/npcsh/npc_team/corca.npc +0 -12
  105. npcsh-1.1.14.data/data/npcsh/npc_team/foreman.npc +0 -7
  106. npcsh-1.1.14.data/data/npcsh/npc_team/frederic.npc +0 -6
  107. npcsh-1.1.14.data/data/npcsh/npc_team/guac.jinx +0 -317
  108. npcsh-1.1.14.data/data/npcsh/npc_team/jinxs.jinx +0 -32
  109. npcsh-1.1.14.data/data/npcsh/npc_team/kadiefa.npc +0 -3
  110. npcsh-1.1.14.data/data/npcsh/npc_team/plonk.jinx +0 -214
  111. npcsh-1.1.14.data/data/npcsh/npc_team/plonk.npc +0 -2
  112. npcsh-1.1.14.data/data/npcsh/npc_team/plonkjr.npc +0 -2
  113. npcsh-1.1.14.data/data/npcsh/npc_team/pti.jinx +0 -170
  114. npcsh-1.1.14.data/data/npcsh/npc_team/sibiji.npc +0 -3
  115. npcsh-1.1.14.data/data/npcsh/npc_team/wander.jinx +0 -186
  116. npcsh-1.1.14.dist-info/RECORD +0 -135
  117. npcsh-1.1.14.dist-info/entry_points.txt +0 -9
  118. npcsh-1.1.14.dist-info/top_level.txt +0 -1
  119. /npcsh/npc_team/jinxs/{utils → bin}/roll.jinx +0 -0
  120. /npcsh/npc_team/jinxs/{utils → bin}/sample.jinx +0 -0
  121. /npcsh/npc_team/jinxs/{modes → bin}/spool.jinx +0 -0
  122. /npcsh/npc_team/jinxs/{modes → bin}/yap.jinx +0 -0
  123. /npcsh/npc_team/jinxs/{utils → lib/computer_use}/trigger.jinx +0 -0
  124. /npcsh/npc_team/jinxs/{utils → lib/core}/chat.jinx +0 -0
  125. /npcsh/npc_team/jinxs/{utils → lib/core}/cmd.jinx +0 -0
  126. /npcsh/npc_team/jinxs/{utils → lib/core}/compress.jinx +0 -0
  127. /npcsh/npc_team/jinxs/{utils → lib/core}/ots.jinx +0 -0
  128. /npcsh/npc_team/jinxs/{code → lib/core}/python.jinx +0 -0
  129. /npcsh/npc_team/jinxs/{utils → lib/core}/sleep.jinx +0 -0
  130. /npcsh/npc_team/jinxs/{utils/core → lib/utils}/compile.jinx +0 -0
  131. /npcsh/npc_team/jinxs/{utils/core → lib/utils}/help.jinx +0 -0
  132. /npcsh/npc_team/jinxs/{utils/core → lib/utils}/init.jinx +0 -0
  133. /npcsh/npc_team/jinxs/{utils → lib/utils}/serve.jinx +0 -0
  134. /npcsh/npc_team/jinxs/{utils/core → lib/utils}/set.jinx +0 -0
  135. /npcsh/npc_team/jinxs/{utils → lib/utils}/usage.jinx +0 -0
  136. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/alicanto.png +0 -0
  137. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/chat.jinx +0 -0
  138. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  139. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/compile.jinx +0 -0
  140. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/compress.jinx +0 -0
  141. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/corca.png +0 -0
  142. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/corca_example.png +0 -0
  143. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/frederic4.png +0 -0
  144. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/guac.png +0 -0
  145. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/help.jinx +0 -0
  146. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/init.jinx +0 -0
  147. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  148. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npc-studio.jinx +0 -0
  149. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  150. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  151. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/ots.jinx +0 -0
  152. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/plonk.png +0 -0
  153. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  154. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/python.jinx +0 -0
  155. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/roll.jinx +0 -0
  156. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sample.jinx +0 -0
  157. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/serve.jinx +0 -0
  158. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/set.jinx +0 -0
  159. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sibiji.png +0 -0
  160. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  161. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/spool.jinx +0 -0
  162. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/spool.png +0 -0
  163. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  164. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/usage.jinx +0 -0
  165. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/yap.jinx +0 -0
  166. {npcsh-1.1.14.data → npcsh-1.1.16.data}/data/npcsh/npc_team/yap.png +0 -0
  167. {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/WHEEL +0 -0
  168. {npcsh-1.1.14.dist-info → npcsh-1.1.16.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,43 @@
1
+ jinx_name: open_browser
2
+ description: |
3
+ Open a browser and navigate to a URL. The browser stays open for follow-up commands.
4
+ Use this to start browser automation.
5
+ inputs:
6
+ - url:
7
+ description: "URL to navigate to"
8
+ - browser: "firefox"
9
+
10
+ steps:
11
+ - name: open_browser
12
+ engine: python
13
+ code: |
14
+ from selenium import webdriver
15
+ from selenium.webdriver.firefox.service import Service as FirefoxService
16
+ from selenium.webdriver.chrome.service import Service as ChromeService
17
+ from webdriver_manager.firefox import GeckoDriverManager
18
+ from webdriver_manager.chrome import ChromeDriverManager
19
+ from npcpy.work.browser import set_driver
20
+ import uuid
21
+
22
+ url = context.get('url', '')
23
+ browser_type = context.get('browser', 'firefox').lower()
24
+
25
+ try:
26
+ if browser_type == 'chrome':
27
+ options = webdriver.ChromeOptions()
28
+ options.add_argument('--start-maximized')
29
+ driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=options)
30
+ else:
31
+ options = webdriver.FirefoxOptions()
32
+ driver = webdriver.Firefox(service=FirefoxService(GeckoDriverManager().install()), options=options)
33
+ driver.maximize_window()
34
+
35
+ driver.get(url)
36
+
37
+ session_id = str(uuid.uuid4())[:8]
38
+ set_driver(session_id, driver)
39
+
40
+ output = "Browser opened. Session: {}. Navigated to: {}. Title: {}".format(session_id, url, driver.title)
41
+ except Exception as e:
42
+ import traceback
43
+ output = "Failed to open browser: {}\n{}".format(str(e), traceback.format_exc())
@@ -0,0 +1,101 @@
1
+ jinx_name: paper_search
2
+ description: Search for academic papers across multiple sources (Semantic Scholar, arXiv, local datasets)
3
+ inputs:
4
+ - query: ""
5
+ - limit: 10
6
+ - source: "all"
7
+ steps:
8
+ - name: search_papers
9
+ engine: python
10
+ code: |
11
+ import os
12
+ import time
13
+ import requests
14
+ import urllib.request
15
+ import urllib.parse
16
+ import xml.etree.ElementTree as ET
17
+
18
+ query = context.get('query', '')
19
+ limit = int(context.get('limit', 10))
20
+ source = context.get('source', 'all').lower()
21
+
22
+ if not query:
23
+ context['output'] = """Usage: /paper_search <query> [--limit N] [--source SOURCE]
24
+
25
+ Sources:
26
+ all - Search all available sources (default)
27
+ s2 - Semantic Scholar only (requires S2_API_KEY)
28
+ arxiv - arXiv only
29
+ """
30
+ exit()
31
+
32
+ all_results = []
33
+
34
+ # Semantic Scholar
35
+ if source in ['all', 's2']:
36
+ api_key = os.environ.get('S2_API_KEY')
37
+ if api_key:
38
+ try:
39
+ url = "https://api.semanticscholar.org/graph/v1/paper/search"
40
+ headers = {"x-api-key": api_key}
41
+ params = {"query": query, "limit": limit, "fields": "title,abstract,authors,year,citationCount,url"}
42
+ response = requests.get(url, headers=headers, params=params, timeout=30)
43
+ response.raise_for_status()
44
+ for paper in response.json().get('data', []):
45
+ all_results.append({
46
+ 'source': 'Semantic Scholar',
47
+ 'title': paper.get('title', ''),
48
+ 'year': paper.get('year'),
49
+ 'citations': paper.get('citationCount', 0),
50
+ 'authors': [a.get('name', '') for a in paper.get('authors', [])],
51
+ 'abstract': paper.get('abstract', '')[:300] if paper.get('abstract') else '',
52
+ 'url': paper.get('url', '')
53
+ })
54
+ except Exception as e:
55
+ print(f"S2 error: {e}")
56
+
57
+ # arXiv
58
+ if source in ['all', 'arxiv']:
59
+ try:
60
+ base_url = "http://export.arxiv.org/api/query"
61
+ params = {"search_query": f"all:{query}", "max_results": limit}
62
+ url = f"{base_url}?{urllib.parse.urlencode(params)}"
63
+ with urllib.request.urlopen(url, timeout=30) as response:
64
+ data = response.read().decode('utf-8')
65
+ root = ET.fromstring(data)
66
+ ns = {'atom': 'http://www.w3.org/2005/Atom'}
67
+ for entry in root.findall('atom:entry', ns):
68
+ all_results.append({
69
+ 'source': 'arXiv',
70
+ 'title': entry.find('atom:title', ns).text.strip().replace('\n', ' '),
71
+ 'year': entry.find('atom:published', ns).text[:4],
72
+ 'citations': None,
73
+ 'authors': [a.find('atom:name', ns).text for a in entry.findall('atom:author', ns)],
74
+ 'abstract': entry.find('atom:summary', ns).text.strip()[:300],
75
+ 'url': entry.find('atom:id', ns).text
76
+ })
77
+ except Exception as e:
78
+ print(f"arXiv error: {e}")
79
+
80
+ if not all_results:
81
+ context['output'] = f"No papers found for: {query}"
82
+ exit()
83
+
84
+ # Format output
85
+ results = []
86
+ for i, paper in enumerate(all_results[:limit], 1):
87
+ authors = ', '.join(paper['authors'][:3])
88
+ if len(paper['authors']) > 3:
89
+ authors += ' et al.'
90
+ year = paper.get('year', '?')
91
+ citations = f", {paper['citations']} citations" if paper.get('citations') else ""
92
+
93
+ results.append(f"{i}. [{paper['source']}] {paper['title']} ({year}{citations})")
94
+ results.append(f" Authors: {authors}")
95
+ if paper['abstract']:
96
+ results.append(f" Abstract: {paper['abstract']}...")
97
+ results.append(f" URL: {paper['url']}")
98
+ results.append("")
99
+
100
+ context['output'] = f"Found {len(all_results)} papers:\n\n" + "\n".join(results)
101
+ context['papers'] = all_results
@@ -0,0 +1,134 @@
1
+ jinx_name: paste
2
+ description: Grabs content from clipboard (images or text) and saves/displays it. Use this when Ctrl+V paste doesn't work properly.
3
+ inputs:
4
+ - output_path:
5
+ default: ""
6
+ description: "Optional path to save image to. If empty, saves to temp file."
7
+ steps:
8
+ - name: "paste_clipboard"
9
+ engine: "python"
10
+ code: |
11
+ import tempfile
12
+ import os
13
+ from datetime import datetime
14
+ import io
15
+
16
+ output_path = ({{ output_path | default("") | tojson }} or "").strip()
17
+ image_saved = False
18
+ text_content = None
19
+
20
+ # Try PIL/Pillow with ImageGrab first (works on most systems)
21
+ try:
22
+ from PIL import ImageGrab, Image
23
+
24
+ # Try to grab image from clipboard
25
+ img = ImageGrab.grabclipboard()
26
+
27
+ if img is not None:
28
+ if isinstance(img, Image.Image):
29
+ # It's an image
30
+ if output_path:
31
+ save_path = os.path.expanduser(output_path)
32
+ else:
33
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
34
+ fd, save_path = tempfile.mkstemp(suffix='.png', prefix=f'npcsh_paste_{timestamp}_')
35
+ os.close(fd)
36
+
37
+ img.save(save_path, 'PNG')
38
+ file_size = os.path.getsize(save_path)
39
+
40
+ if file_size > 1024 * 1024:
41
+ size_str = f"{file_size / (1024*1024):.1f} MB"
42
+ elif file_size > 1024:
43
+ size_str = f"{file_size / 1024:.1f} KB"
44
+ else:
45
+ size_str = f"{file_size} bytes"
46
+
47
+ output = f"Image saved to: {save_path} ({size_str}, {img.size[0]}x{img.size[1]})"
48
+ context['pasted_image_path'] = save_path
49
+ image_saved = True
50
+
51
+ elif isinstance(img, list):
52
+ # It's a list of file paths (copied files)
53
+ output = f"Clipboard contains {len(img)} file(s):\n" + "\n".join(img)
54
+ context['pasted_files'] = img
55
+ image_saved = True
56
+
57
+ except ImportError:
58
+ pass
59
+ except Exception as e:
60
+ # ImageGrab failed, try other methods
61
+ pass
62
+
63
+ # If no image, try to get text via tkinter
64
+ if not image_saved:
65
+ try:
66
+ import tkinter as tk
67
+
68
+ root = tk.Tk()
69
+ root.withdraw()
70
+
71
+ try:
72
+ text_content = root.clipboard_get()
73
+ except tk.TclError:
74
+ text_content = None
75
+
76
+ root.destroy()
77
+
78
+ except Exception as e:
79
+ pass
80
+
81
+ # If still nothing, try GTK
82
+ if not image_saved and text_content is None:
83
+ try:
84
+ import gi
85
+ gi.require_version('Gtk', '3.0')
86
+ from gi.repository import Gtk, Gdk, GdkPixbuf
87
+
88
+ clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
89
+
90
+ # Try image first
91
+ pixbuf = clipboard.wait_for_image()
92
+ if pixbuf:
93
+ if output_path:
94
+ save_path = os.path.expanduser(output_path)
95
+ else:
96
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
97
+ fd, save_path = tempfile.mkstemp(suffix='.png', prefix=f'npcsh_paste_{timestamp}_')
98
+ os.close(fd)
99
+
100
+ pixbuf.savev(save_path, 'png', [], [])
101
+ file_size = os.path.getsize(save_path)
102
+
103
+ if file_size > 1024 * 1024:
104
+ size_str = f"{file_size / (1024*1024):.1f} MB"
105
+ elif file_size > 1024:
106
+ size_str = f"{file_size / 1024:.1f} KB"
107
+ else:
108
+ size_str = f"{file_size} bytes"
109
+
110
+ output = f"Image saved to: {save_path} ({size_str}, {pixbuf.get_width()}x{pixbuf.get_height()})"
111
+ context['pasted_image_path'] = save_path
112
+ image_saved = True
113
+ else:
114
+ # Try text
115
+ text_content = clipboard.wait_for_text()
116
+
117
+ except Exception as e:
118
+ pass
119
+
120
+ # Handle text content
121
+ if not image_saved and text_content:
122
+ line_count = text_content.count('\n') + 1
123
+ char_count = len(text_content)
124
+ context['pasted_text'] = text_content
125
+
126
+ if line_count > 10:
127
+ preview_lines = text_content.split('\n')[:10]
128
+ preview = '\n'.join(preview_lines)
129
+ output = f"Clipboard text ({line_count} lines, {char_count} chars):\n---\n{preview}\n... ({line_count - 10} more lines)\n---"
130
+ else:
131
+ output = f"Clipboard text ({line_count} lines, {char_count} chars):\n---\n{text_content}\n---"
132
+
133
+ elif not image_saved:
134
+ output = "Clipboard is empty or could not access clipboard.\nMake sure you have PIL/Pillow installed: pip install Pillow"
@@ -0,0 +1,27 @@
1
+ name: plonk
2
+ ascii_art: |
3
+ 🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲
4
+ ██████ ██ ██████ ███ ██ ██ ██
5
+ ██ ██ ██ ██ ██ ████ ██ ██ ██
6
+ ██████ ██ 🪵 ██ ██ ██ ██ ██ █████
7
+ ██ ██ ██ ██ ██ ██ ██ ██ ██
8
+ ██ ███████ ██████ ██ ████ ██ ██
9
+ 🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲
10
+ colors:
11
+ top: "34,139,34"
12
+ bottom: "139,69,19"
13
+ primary_directive: |
14
+ You are plonk, the browser and GUI automation specialist.
15
+
16
+ Browser tools: open_browser, browser_action, browser_screenshot, close_browser
17
+
18
+ browser_action actions: click, type, type_and_enter, select, wait, scroll, get_text, get_page, get_elements, press_key
19
+
20
+ Use get_elements to discover selectors on the page. Use xpath:// prefix for XPath selectors.
21
+
22
+ Desktop tools: screenshot, click, type_text, key_press, launch_app, wait
23
+ jinxs:
24
+ - lib/browser/*
25
+ - lib/computer_use/*
26
+ - lib/core/sh
27
+ - lib/core/python
@@ -0,0 +1,23 @@
1
+ name: plonkjr
2
+ ascii_art: |
3
+ 🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲
4
+ ██████ ██ ██████ ███ ██ ██ ██ 🪵 ██ ██████
5
+ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██
6
+ ██████ ██ ██ ██ ██ ██ ██ █████ 🌲 ██ ██████
7
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
8
+ ██ ███████ ██████ ██ ████ ██ ██ █████ ██ ██
9
+ 🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲
10
+ colors:
11
+ top: "34,139,34"
12
+ bottom: "139,69,19"
13
+ primary_directive: |
14
+ You are plonkjr, the junior automation assistant. You handle basic computer use tasks:
15
+ taking screenshots, clicking, typing, and pressing keys. You're simpler than plonk -
16
+ you focus on executing the core actions without complex vision-based planning.
17
+ Just do what's asked: screenshot, click, type, key press.
18
+ jinxs:
19
+ - lib/computer_use/screenshot
20
+ - lib/computer_use/click
21
+ - lib/computer_use/type_text
22
+ - lib/computer_use/key_press
23
+ - lib/core/sh
@@ -0,0 +1,23 @@
1
+ jinx_name: screenshot
2
+ description: Capture a screenshot of the current screen
3
+ inputs:
4
+ - output_path: null # Optional path to save screenshot
5
+
6
+ steps:
7
+ - name: capture_screenshot
8
+ engine: python
9
+ code: |
10
+ from npcpy.data.image import capture_screenshot
11
+
12
+ output_path = context.get('output_path')
13
+ messages = context.get('messages', [])
14
+
15
+ result = capture_screenshot(full=True)
16
+
17
+ if result and 'file_path' in result:
18
+ context['output'] = f"Screenshot saved to: {result['file_path']}"
19
+ context['screenshot_path'] = result['file_path']
20
+ else:
21
+ context['output'] = "Failed to capture screenshot"
22
+
23
+ context['messages'] = messages
@@ -25,7 +25,8 @@ steps:
25
25
  code: |
26
26
  import os
27
27
  import traceback
28
-
28
+ from npcpy.data.web import search_web
29
+
29
30
  # Access query from context
30
31
  query = context.get('query')
31
32
  if not query or not query.strip():
@@ -0,0 +1,69 @@
1
+ jinx_name: semantic_scholar
2
+ description: Search Semantic Scholar for academic papers. Requires S2_API_KEY env var.
3
+ inputs:
4
+ - query: ""
5
+ - limit: 10
6
+ steps:
7
+ - name: search_s2
8
+ engine: python
9
+ code: |
10
+ import os
11
+ import time
12
+ import requests
13
+
14
+ query = context.get('query', '')
15
+ limit = int(context.get('limit', 10))
16
+
17
+ if not query:
18
+ context['output'] = "Usage: /semantic_scholar <query> [--limit N]"
19
+ exit()
20
+
21
+ api_key = os.environ.get('S2_API_KEY')
22
+ if not api_key:
23
+ context['output'] = "Error: S2_API_KEY environment variable not set. Get one at https://www.semanticscholar.org/product/api"
24
+ exit()
25
+
26
+ url = "https://api.semanticscholar.org/graph/v1/paper/search"
27
+ headers = {"x-api-key": api_key}
28
+ params = {
29
+ "query": query,
30
+ "limit": limit,
31
+ "fields": "title,abstract,authors,year,citationCount,url,tldr"
32
+ }
33
+
34
+ try:
35
+ response = requests.get(url, headers=headers, params=params, timeout=30)
36
+ response.raise_for_status()
37
+ data = response.json().get('data', [])
38
+
39
+ if not data:
40
+ context['output'] = f"No papers found for: {query}"
41
+ exit()
42
+
43
+ results = []
44
+ for i, paper in enumerate(data, 1):
45
+ title = paper.get('title', 'No title')
46
+ year = paper.get('year', '?')
47
+ citations = paper.get('citationCount', 0)
48
+ authors = ', '.join([a.get('name', '') for a in paper.get('authors', [])[:3]])
49
+ if len(paper.get('authors', [])) > 3:
50
+ authors += ' et al.'
51
+ abstract = paper.get('abstract', '')[:200] + '...' if paper.get('abstract') else 'No abstract'
52
+ tldr = paper.get('tldr', {}).get('text', '') if paper.get('tldr') else ''
53
+ url = paper.get('url', '')
54
+
55
+ results.append(f"{i}. {title} ({year})")
56
+ results.append(f" Authors: {authors}")
57
+ results.append(f" Citations: {citations}")
58
+ if tldr:
59
+ results.append(f" TL;DR: {tldr}")
60
+ else:
61
+ results.append(f" Abstract: {abstract}")
62
+ results.append(f" URL: {url}")
63
+ results.append("")
64
+
65
+ context['output'] = f"Found {len(data)} papers:\n\n" + "\n".join(results)
66
+ context['papers'] = data
67
+
68
+ except requests.exceptions.RequestException as e:
69
+ context['output'] = f"Semantic Scholar API error: {e}"
@@ -1,5 +1,5 @@
1
1
  jinx_name: sh
2
- description: Execute bash queries. Should be used to grep for file contents, list directories, explore information to answer user questions more practically.
2
+ description: Execute bash queries. Should be used to grep for file contents, list directories, explore information to answer user questions more practically. NEVER use ls -R on directories that may contain node_modules, .git, or other large dependency folders - this will exceed token limits. Use targeted ls commands instead.
3
3
  inputs:
4
4
  - bash_command
5
5
  steps:
@@ -9,7 +9,7 @@ steps:
9
9
  import subprocess
10
10
  import os
11
11
 
12
- cmd = '{{ bash_command }}'
12
+ cmd = {{ bash_command | tojson }}
13
13
  output = ""
14
14
 
15
15
  process = subprocess.Popen(
@@ -20,12 +20,6 @@ steps:
20
20
  )
21
21
  stdout, stderr = process.communicate()
22
22
 
23
- # Only show debug output if NPCSH_DEBUG is set
24
- if os.environ.get("NPCSH_DEBUG") == "1":
25
- import sys
26
- print(f"[sh] cmd: {cmd}", file=sys.stderr)
27
- print(f"[sh] stdout: {stdout.decode('utf-8', errors='ignore')[:200]}", file=sys.stderr)
28
-
29
23
  if stderr:
30
24
  output = f"Error: {stderr.decode('utf-8')}"
31
25
  else:
@@ -0,0 +1,17 @@
1
+ jinx_name: "shh"
2
+ description: "Enable silent mode - only shows warnings and errors"
3
+ inputs: []
4
+ steps:
5
+ - name: "set_silent"
6
+ engine: "python"
7
+ code: |
8
+ state = context.get('state')
9
+ output_messages = context.get('messages', [])
10
+
11
+ if state:
12
+ result = state.set_log_level("silent")
13
+ context['output'] = result
14
+ else:
15
+ context['output'] = "Error: state not available"
16
+
17
+ context['messages'] = output_messages
@@ -0,0 +1,24 @@
1
+ name: sibiji
2
+ ascii_art: |
3
+ 🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️
4
+ ██████ ██ ██████ ██ ██ ██████ ██
5
+ ██ ██ ██ ██ ██ ██ ██ ██
6
+ █████ ██ ██████ ██ 🕷️ ██ ██ ██
7
+ ██ ██ ██ ██ ██ ██ 🕷️ ██ ██
8
+ ██████ ██ ██████ ██ ██ ██████ ██
9
+ 🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️🕸️
10
+ colors:
11
+ top: "148,0,211"
12
+ bottom: "75,0,130"
13
+ primary_directive: |
14
+ You are sibiji, the orchestrator and general manager of the NPC team.
15
+ Your role is to delegate tasks to appropriate specialist agents based on their expertise.
16
+
17
+ When delegating, match the task to the agent whose primary_directive best fits.
18
+ You have access to the delegate tool to pass tasks to other agents.
19
+ jinxs:
20
+ - lib/orchestration/delegate
21
+ - lib/orchestration/convene
22
+ - lib/core/sh
23
+ - lib/core/python
24
+ - lib/core/search
@@ -8,7 +8,7 @@ steps:
8
8
  - engine: python
9
9
  code: |
10
10
  import pandas as pd
11
- query = "{{ sql_query }}"
11
+ query = {{ sql_query | tojson }}
12
12
  try:
13
13
  df = pd.read_sql_query(query, npc.db_conn)
14
14
  except Exception as e:
@@ -0,0 +1,62 @@
1
+ jinx_name: switch
2
+ description: Get or set a switch in the .ctx file
3
+ inputs:
4
+ - name: "" # Switch name
5
+ - value: null # Value to set (omit to get current value)
6
+ - scope: "workspace" # "workspace" or "global"
7
+
8
+ steps:
9
+ - name: manage_switch
10
+ engine: python
11
+ code: |
12
+ import os
13
+ import yaml
14
+ from pathlib import Path
15
+
16
+ switch_name = context.get('name', '')
17
+ switch_value = context.get('value')
18
+ scope = context.get('scope', 'workspace')
19
+ messages = context.get('messages', [])
20
+
21
+ if not switch_name:
22
+ context['output'] = "Usage: /switch <name> [value] [--scope workspace|global]"
23
+ context['messages'] = messages
24
+ exit()
25
+
26
+ # Determine .ctx path based on scope
27
+ if scope == "global":
28
+ ctx_path = Path.home() / ".npcsh" / "npc_team" / "npcsh.ctx"
29
+ else:
30
+ cwd = os.getcwd()
31
+ # Look for npc_team/*.ctx in workspace
32
+ npc_team_dir = Path(cwd) / "npc_team"
33
+ ctx_files = list(npc_team_dir.glob("*.ctx")) if npc_team_dir.exists() else []
34
+ ctx_path = ctx_files[0] if ctx_files else npc_team_dir / "team.ctx"
35
+
36
+ # Load existing ctx
37
+ ctx_data = {}
38
+ if ctx_path.exists():
39
+ try:
40
+ with open(ctx_path) as f:
41
+ ctx_data = yaml.safe_load(f) or {}
42
+ except:
43
+ ctx_data = {}
44
+
45
+ # Ensure switches dict exists
46
+ if 'switches' not in ctx_data:
47
+ ctx_data['switches'] = {}
48
+
49
+ if switch_value is None:
50
+ # Get mode
51
+ current = ctx_data['switches'].get(switch_name, "not set")
52
+ context['output'] = f"{switch_name} ({scope}): {current}"
53
+ else:
54
+ # Set mode
55
+ ctx_path.parent.mkdir(parents=True, exist_ok=True)
56
+ ctx_data['switches'][switch_name] = switch_value
57
+ with open(ctx_path, 'w') as f:
58
+ yaml.dump(ctx_data, f, default_flow_style=False)
59
+ context['output'] = f"Set {switch_name} = {switch_value} ({scope})"
60
+
61
+ context['messages'] = messages
62
+ context['switch_value'] = ctx_data['switches'].get(switch_name)
@@ -0,0 +1,61 @@
1
+ jinx_name: switches
2
+ description: List all switches from .ctx files
3
+ inputs:
4
+ - scope: "all" # "workspace", "global", or "all"
5
+
6
+ steps:
7
+ - name: list_switches
8
+ engine: python
9
+ code: |
10
+ import os
11
+ import yaml
12
+ from pathlib import Path
13
+
14
+ scope = context.get('scope', 'all')
15
+ messages = context.get('messages', [])
16
+
17
+ output_lines = []
18
+
19
+ # Global switches
20
+ if scope in ["global", "all"]:
21
+ global_ctx = Path.home() / ".npcsh" / "npc_team" / "npcsh.ctx"
22
+ if global_ctx.exists():
23
+ try:
24
+ with open(global_ctx) as f:
25
+ data = yaml.safe_load(f) or {}
26
+ switches = data.get('switches', {})
27
+ if switches:
28
+ output_lines.append("Global switches:")
29
+ for k, v in switches.items():
30
+ output_lines.append(f" {k}: {v}")
31
+ else:
32
+ output_lines.append("Global switches: (none)")
33
+ except:
34
+ output_lines.append("Global switches: (none)")
35
+ else:
36
+ output_lines.append("Global switches: (none)")
37
+
38
+ # Workspace switches
39
+ if scope in ["workspace", "all"]:
40
+ cwd = os.getcwd()
41
+ npc_team_dir = Path(cwd) / "npc_team"
42
+ ctx_files = list(npc_team_dir.glob("*.ctx")) if npc_team_dir.exists() else []
43
+
44
+ if ctx_files:
45
+ try:
46
+ with open(ctx_files[0]) as f:
47
+ data = yaml.safe_load(f) or {}
48
+ switches = data.get('switches', {})
49
+ if switches:
50
+ output_lines.append(f"Workspace switches:")
51
+ for k, v in switches.items():
52
+ output_lines.append(f" {k}: {v}")
53
+ else:
54
+ output_lines.append("Workspace switches: (none)")
55
+ except:
56
+ output_lines.append("Workspace switches: (none)")
57
+ else:
58
+ output_lines.append("Workspace switches: (none)")
59
+
60
+ context['output'] = "\n".join(output_lines) if output_lines else "No switches configured."
61
+ context['messages'] = messages