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,76 @@
1
+ jinx_name: arxiv
2
+ description: Search arXiv for preprints and papers
3
+ inputs:
4
+ - query: ""
5
+ - limit: 10
6
+ steps:
7
+ - name: search_arxiv
8
+ engine: python
9
+ code: |
10
+ import urllib.request
11
+ import urllib.parse
12
+ import xml.etree.ElementTree as ET
13
+
14
+ query = context.get('query', '')
15
+ limit = int(context.get('limit', 10))
16
+
17
+ if not query:
18
+ context['output'] = "Usage: /arxiv <query> [--limit N]"
19
+ exit()
20
+
21
+ base_url = "http://export.arxiv.org/api/query"
22
+ params = {
23
+ "search_query": f"all:{query}",
24
+ "start": 0,
25
+ "max_results": limit,
26
+ "sortBy": "relevance",
27
+ "sortOrder": "descending"
28
+ }
29
+
30
+ url = f"{base_url}?{urllib.parse.urlencode(params)}"
31
+
32
+ try:
33
+ with urllib.request.urlopen(url, timeout=30) as response:
34
+ data = response.read().decode('utf-8')
35
+
36
+ root = ET.fromstring(data)
37
+ ns = {'atom': 'http://www.w3.org/2005/Atom'}
38
+
39
+ entries = root.findall('atom:entry', ns)
40
+
41
+ if not entries:
42
+ context['output'] = f"No papers found for: {query}"
43
+ exit()
44
+
45
+ results = []
46
+ papers = []
47
+ for i, entry in enumerate(entries, 1):
48
+ title = entry.find('atom:title', ns).text.strip().replace('\n', ' ')
49
+ summary = entry.find('atom:summary', ns).text.strip()[:300] + '...'
50
+ published = entry.find('atom:published', ns).text[:10]
51
+ authors = [a.find('atom:name', ns).text for a in entry.findall('atom:author', ns)]
52
+ author_str = ', '.join(authors[:3])
53
+ if len(authors) > 3:
54
+ author_str += ' et al.'
55
+ link = entry.find('atom:id', ns).text
56
+
57
+ results.append(f"{i}. {title}")
58
+ results.append(f" Authors: {author_str}")
59
+ results.append(f" Published: {published}")
60
+ results.append(f" Abstract: {summary}")
61
+ results.append(f" URL: {link}")
62
+ results.append("")
63
+
64
+ papers.append({
65
+ 'title': title,
66
+ 'authors': authors,
67
+ 'abstract': entry.find('atom:summary', ns).text.strip(),
68
+ 'published': published,
69
+ 'url': link
70
+ })
71
+
72
+ context['output'] = f"Found {len(entries)} papers on arXiv:\n\n" + "\n".join(results)
73
+ context['papers'] = papers
74
+
75
+ except Exception as e:
76
+ context['output'] = f"arXiv search error: {e}"
@@ -0,0 +1,220 @@
1
+ jinx_name: browser_action
2
+ description: |
3
+ Perform an action in the browser. Actions:
4
+ - click: Click element
5
+ - type: Type text into element (clears first)
6
+ - type_and_enter: Type text and press Enter
7
+ - set_value: Force set value via JS (bypasses date pickers/validation)
8
+ - select: Select dropdown option by visible text
9
+ - wait: Wait for element to appear
10
+ - scroll: Scroll page (up/down/to element)
11
+ - get_text: Get text from element
12
+ - get_page: Get page title, URL, and visible text
13
+ - get_elements: Get interactive elements with their selectors
14
+ - press_key: Press a key (enter, tab, escape, etc)
15
+ Selectors: CSS (#id, .class, input[name="x"]) or xpath://... for XPath
16
+ inputs:
17
+ - action:
18
+ description: "Action: click, type, type_and_enter, set_value, select, wait, scroll, get_text, get_page, get_elements, press_key"
19
+ - selector:
20
+ description: "CSS selector or XPath (prefix xpath: for XPath)"
21
+ default: ""
22
+ - value:
23
+ description: "Value for type/select, or scroll direction, or key name"
24
+ default: ""
25
+
26
+ steps:
27
+ - name: browser_action
28
+ engine: python
29
+ code: |
30
+ from selenium.webdriver.common.by import By
31
+ from selenium.webdriver.support.ui import WebDriverWait, Select
32
+ from selenium.webdriver.support import expected_conditions as EC
33
+ from selenium.webdriver.common.keys import Keys
34
+ from npcpy.work.browser import get_current_driver
35
+
36
+ action = context.get('action', '').lower()
37
+ selector = context.get('selector', '')
38
+ value = context.get('value', '')
39
+
40
+ driver = get_current_driver()
41
+ if not driver:
42
+ output = "No active browser. Use open_browser first."
43
+ exit()
44
+
45
+ def find_element(sel):
46
+ if sel.startswith('xpath:'):
47
+ return driver.find_element(By.XPATH, sel[6:])
48
+ else:
49
+ return driver.find_element(By.CSS_SELECTOR, sel)
50
+
51
+ def wait_for_element(sel, timeout=10):
52
+ if sel.startswith('xpath:'):
53
+ return WebDriverWait(driver, timeout).until(
54
+ EC.presence_of_element_located((By.XPATH, sel[6:]))
55
+ )
56
+ else:
57
+ return WebDriverWait(driver, timeout).until(
58
+ EC.presence_of_element_located((By.CSS_SELECTOR, sel))
59
+ )
60
+
61
+ try:
62
+ if action == 'click':
63
+ elem = wait_for_element(selector)
64
+ elem.click()
65
+ output = "Clicked: {}".format(selector)
66
+
67
+ elif action == 'type':
68
+ elem = wait_for_element(selector)
69
+ elem.click() # Focus first
70
+ elem.clear()
71
+ elem.send_keys(value)
72
+ output = "Typed '{}' into {}".format(value, selector)
73
+
74
+ elif action == 'set_value':
75
+ # Force set value via JS, bypasses validation/calendars
76
+ elem = wait_for_element(selector)
77
+ driver.execute_script("arguments[0].value = arguments[1]; arguments[0].dispatchEvent(new Event('input', {bubbles: true})); arguments[0].dispatchEvent(new Event('change', {bubbles: true}));", elem, value)
78
+ output = "Set value '{}' on {}".format(value, selector)
79
+
80
+ elif action == 'type_and_enter':
81
+ elem = wait_for_element(selector)
82
+ elem.clear()
83
+ elem.send_keys(value)
84
+ elem.send_keys(Keys.RETURN)
85
+ output = "Typed '{}' and pressed Enter".format(value)
86
+
87
+ elif action == 'select':
88
+ elem = wait_for_element(selector)
89
+ select = Select(elem)
90
+ select.select_by_visible_text(value)
91
+ output = "Selected '{}' in {}".format(value, selector)
92
+
93
+ elif action == 'wait':
94
+ timeout = int(value) if value else 10
95
+ wait_for_element(selector, timeout)
96
+ output = "Element found: {}".format(selector)
97
+
98
+ elif action == 'scroll':
99
+ if value == 'down':
100
+ driver.execute_script("window.scrollBy(0, 500)")
101
+ elif value == 'up':
102
+ driver.execute_script("window.scrollBy(0, -500)")
103
+ elif selector:
104
+ elem = find_element(selector)
105
+ driver.execute_script("arguments[0].scrollIntoView();", elem)
106
+ output = "Scrolled {}".format(value or 'to element')
107
+
108
+ elif action == 'get_text':
109
+ elem = wait_for_element(selector)
110
+ output = "Text: {}".format(elem.text)
111
+
112
+ elif action == 'get_page':
113
+ title = driver.title
114
+ url = driver.current_url
115
+ body = driver.find_element(By.TAG_NAME, 'body')
116
+ text = body.text[:3000]
117
+ output = "Page: {} ({})\n\nContent:\n{}".format(title, url, text)
118
+
119
+ elif action == 'get_elements':
120
+ elements = []
121
+
122
+ def is_visible(el):
123
+ try:
124
+ return el.is_displayed() and el.size['width'] > 0
125
+ except:
126
+ return False
127
+
128
+ def safe_selector(tag, el):
129
+ eid = el.get_attribute('id')
130
+ name = el.get_attribute('name')
131
+ if eid and '.' not in eid and ' ' not in eid:
132
+ return '#' + eid
133
+ elif eid:
134
+ return '{}[id="{}"]'.format(tag, eid)
135
+ elif name:
136
+ return '{}[name="{}"]'.format(tag, name)
137
+ return None
138
+
139
+ # Get inputs
140
+ for inp in driver.find_elements(By.CSS_SELECTOR, 'input:not([type="hidden"])'):
141
+ if not is_visible(inp):
142
+ continue
143
+ sel = safe_selector('input', inp)
144
+ if not sel:
145
+ ph = inp.get_attribute('placeholder')
146
+ if ph:
147
+ sel = 'input[placeholder="{}"]'.format(ph)
148
+ else:
149
+ continue
150
+ info = {'tag': 'input', 'type': inp.get_attribute('type') or 'text', 'selector': sel}
151
+ info['placeholder'] = inp.get_attribute('placeholder') or ''
152
+ elements.append(info)
153
+
154
+ # Get buttons
155
+ for btn in driver.find_elements(By.CSS_SELECTOR, 'button, input[type="submit"], input[type="button"]'):
156
+ if not is_visible(btn):
157
+ continue
158
+ sel = safe_selector('button', btn)
159
+ if not sel and btn.text:
160
+ sel = 'xpath://button[contains(text(),"{}")]'.format(btn.text[:30])
161
+ if not sel:
162
+ continue
163
+ info = {'tag': 'button', 'selector': sel, 'text': (btn.text or '')[:50]}
164
+ elements.append(info)
165
+
166
+ # Get select dropdowns
167
+ for s in driver.find_elements(By.TAG_NAME, 'select'):
168
+ if not is_visible(s):
169
+ continue
170
+ sel = safe_selector('select', s)
171
+ if not sel:
172
+ continue
173
+ opts = [o.text for o in s.find_elements(By.TAG_NAME, 'option')[:5]]
174
+ info = {'tag': 'select', 'selector': sel, 'options': opts}
175
+ elements.append(info)
176
+
177
+ # Get links
178
+ for link in driver.find_elements(By.TAG_NAME, 'a')[:30]:
179
+ if not is_visible(link) or not link.text or len(link.text) < 2:
180
+ continue
181
+ sel = safe_selector('a', link)
182
+ if not sel:
183
+ sel = 'xpath://a[contains(text(),"{}")]'.format(link.text[:30])
184
+ info = {'tag': 'a', 'selector': sel, 'text': link.text[:50]}
185
+ elements.append(info)
186
+
187
+ output = "Found {} visible elements:\n".format(len(elements))
188
+ for el in elements[:40]:
189
+ output += "{}: {} ".format(el['tag'], el.get('selector', ''))
190
+ if el.get('text'):
191
+ output += '"{}" '.format(el['text'][:30])
192
+ if el.get('placeholder'):
193
+ output += 'placeholder="{}" '.format(el['placeholder'])
194
+ if el.get('options'):
195
+ output += "opts={} ".format(el['options'][:3])
196
+ output += "\n"
197
+
198
+ elif action == 'press_key':
199
+ key_map = {
200
+ 'enter': Keys.RETURN, 'return': Keys.RETURN,
201
+ 'tab': Keys.TAB,
202
+ 'escape': Keys.ESCAPE, 'esc': Keys.ESCAPE,
203
+ 'down': Keys.DOWN, 'up': Keys.UP,
204
+ 'left': Keys.LEFT, 'right': Keys.RIGHT,
205
+ 'backspace': Keys.BACKSPACE,
206
+ 'delete': Keys.DELETE,
207
+ }
208
+ key = key_map.get(value.lower(), value)
209
+ if selector:
210
+ elem = find_element(selector)
211
+ elem.send_keys(key)
212
+ else:
213
+ driver.find_element(By.TAG_NAME, 'body').send_keys(key)
214
+ output = "Pressed key: {}".format(value)
215
+
216
+ else:
217
+ output = "Unknown action: {}".format(action)
218
+
219
+ except Exception as e:
220
+ output = "Browser action failed: {}".format(str(e))
@@ -0,0 +1,40 @@
1
+ jinx_name: browser_screenshot
2
+ description: Take a screenshot of the current browser page.
3
+ inputs:
4
+ - filename:
5
+ description: "Optional filename for screenshot"
6
+ default: ""
7
+
8
+ steps:
9
+ - name: browser_screenshot
10
+ engine: python
11
+ code: |
12
+ import os
13
+ from datetime import datetime
14
+ from npcpy.work.browser import get_current_driver
15
+
16
+ filename = context.get('filename', '')
17
+
18
+ driver = get_current_driver()
19
+ if not driver:
20
+ output = "No active browser. Use open_browser first."
21
+ exit()
22
+
23
+ try:
24
+ screenshots_dir = os.path.expanduser('~/.npcsh/screenshots')
25
+ os.makedirs(screenshots_dir, exist_ok=True)
26
+
27
+ if not filename:
28
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
29
+ filename = "browser_{}.png".format(timestamp)
30
+
31
+ if not filename.endswith('.png'):
32
+ filename += '.png'
33
+
34
+ filepath = os.path.join(screenshots_dir, filename)
35
+ driver.save_screenshot(filepath)
36
+
37
+ output = "Screenshot saved: {}".format(filepath)
38
+
39
+ except Exception as e:
40
+ output = "Screenshot failed: {}".format(str(e))
@@ -2,7 +2,7 @@ jinx_name: "build"
2
2
  description: "Build deployment artifacts for NPC team"
3
3
  inputs:
4
4
  - target: "flask" # The type of deployment target (e.g., flask, docker, cli, static).
5
- - output: "./build" # The output directory for built artifacts.
5
+ - outdir: "./build" # The output directory for built artifacts.
6
6
  - team: "./npc_team" # The path to the NPC team directory.
7
7
  - port: 5337 # The port for flask server builds.
8
8
  - cors: "" # Comma-separated CORS origins for flask server builds.
@@ -28,13 +28,13 @@ steps:
28
28
  def build_cli_executable(config, **kwargs): return {"output": f"Mock build cli: {config}", "messages": []}
29
29
  def build_static_site(config, **kwargs): return {"output": f"Mock build static: {config}", "messages": []}
30
30
 
31
- target = context.get('target')
32
- output_dir = context.get('output')
33
- team_path = context.get('team')
34
- port = context.get('port')
35
- cors_origins_str = context.get('cors')
36
-
37
- cors_origins = [origin.strip() for origin in cors_origins_str.split(',')] if cors_origins_str.strip() else None
31
+ target = context.get('target') or 'flask'
32
+ output_dir = context.get('outdir') or './build'
33
+ team_path = context.get('team') or './npc_team'
34
+ port = context.get('port') or 5337
35
+ cors_origins_str = context.get('cors') or ''
36
+
37
+ cors_origins = [origin.strip() for origin in cors_origins_str.split(',') if origin.strip()] or None
38
38
 
39
39
  build_config = {
40
40
  'team_path': os.path.abspath(os.path.expanduser(team_path)),
@@ -0,0 +1,23 @@
1
+ jinx_name: click
2
+ description: Click at screen coordinates (0-100 percentage)
3
+ inputs:
4
+ - x: 50 # X coordinate as percentage (0-100)
5
+ - y: 50 # Y coordinate as percentage (0-100)
6
+
7
+ steps:
8
+ - name: perform_click
9
+ engine: python
10
+ code: |
11
+ from npcpy.work.desktop import perform_action
12
+
13
+ x = float(context.get('x', 50))
14
+ y = float(context.get('y', 50))
15
+ messages = context.get('messages', [])
16
+
17
+ try:
18
+ perform_action({'type': 'click', 'x': x, 'y': y})
19
+ context['output'] = f"Clicked at ({x}%, {y}%)"
20
+ except Exception as e:
21
+ context['output'] = f"Click failed: {e}"
22
+
23
+ context['messages'] = messages
@@ -0,0 +1,14 @@
1
+ jinx_name: close_browser
2
+ description: Close the current browser session.
3
+ inputs: []
4
+
5
+ steps:
6
+ - name: close_browser
7
+ engine: python
8
+ code: |
9
+ from npcpy.work.browser import close_current
10
+
11
+ if close_current():
12
+ output = "Browser closed."
13
+ else:
14
+ output = "No active browser session."
@@ -0,0 +1,232 @@
1
+ jinx_name: convene
2
+ description: Run a cycle of discussions between NPCs on a topic. The orchestrator convenes agents to discuss and synthesize.
3
+ inputs:
4
+ - topic: ""
5
+ - npcs: "alicanto,corca,guac"
6
+ - rounds: 3
7
+ - model: null
8
+ - provider: null
9
+ steps:
10
+ - name: convene_discussion
11
+ engine: python
12
+ code: |
13
+ from termcolor import colored
14
+ from npcpy.llm_funcs import get_llm_response
15
+
16
+ topic = context.get('topic', '')
17
+ npcs_str = context.get('npcs', 'alicanto,corca,guac')
18
+ rounds = int(context.get('rounds', 3))
19
+
20
+ npc = context.get('npc')
21
+ team = context.get('team')
22
+ messages = context.get('messages', [])
23
+
24
+ model = context.get('model') or (npc.model if npc else 'gemini-1.5-flash')
25
+ provider = context.get('provider') or (npc.provider if npc else 'gemini')
26
+
27
+ if not topic:
28
+ context['output'] = """Usage: /convene <topic>
29
+
30
+ Options:
31
+ --npcs LIST Comma-separated NPC names (default: alicanto,corca,guac)
32
+ --rounds N Number of discussion rounds (default: 3)
33
+
34
+ Example: /convene "How should we approach the database migration?" --npcs corca,guac,frederic
35
+ """
36
+ exit()
37
+
38
+ npc_names = [n.strip() for n in npcs_str.split(',')]
39
+
40
+ print(f"""
41
+ ██████ ██████ ███ ██ ██ ██ ███████ ███ ██ ███████
42
+ ██ ██ ██ ████ ██ ██ ██ ██ ████ ██ ██
43
+ ██ ██ ██ ██ ██ ██ ██ ██ █████ ██ ██ ██ █████
44
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
45
+ ██████ ██████ ██ ████ ████ ███████ ██ ████ ███████
46
+
47
+ Convening Discussion
48
+ Topic: {topic}
49
+ Participants: {', '.join(npc_names)}
50
+ Rounds: {rounds}
51
+ """)
52
+
53
+ # Get NPC personas
54
+ participants = []
55
+ for name in npc_names:
56
+ if team and hasattr(team, 'npcs') and name in team.npcs:
57
+ target_npc = team.npcs[name]
58
+ persona = getattr(target_npc, 'primary_directive', f'{name} specialist')
59
+ participants.append({'name': name, 'persona': persona, 'npc': target_npc})
60
+ else:
61
+ participants.append({'name': name, 'persona': f'{name} - general assistant', 'npc': None})
62
+
63
+ import random
64
+
65
+ discussion_log = []
66
+
67
+ for round_num in range(1, rounds + 1):
68
+ print(colored(f"\n{'='*60}", "cyan"))
69
+ print(colored(f" ROUND {round_num}/{rounds}", "cyan", attrs=["bold"]))
70
+ print(colored(f"{'='*60}", "cyan"))
71
+
72
+ round_contributions = []
73
+
74
+ for participant in participants:
75
+ name = participant['name']
76
+ persona = participant['persona']
77
+
78
+ # Build context from previous contributions
79
+ prev_context = ""
80
+ if discussion_log:
81
+ prev_context = "\n\nPrevious discussion:\n"
82
+ for entry in discussion_log[-len(participants)*2:]:
83
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
84
+
85
+ if round_contributions:
86
+ prev_context += "\nThis round so far:\n"
87
+ for entry in round_contributions:
88
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
89
+
90
+ prompt = f"""You are {name}. {persona}
91
+
92
+ Topic under discussion: "{topic}"
93
+ {prev_context}
94
+
95
+ Provide your perspective on this topic. Be concise but insightful.
96
+ Build on what others have said if applicable.
97
+ If you disagree with something, explain why constructively.
98
+ """
99
+
100
+ print(colored(f"\n[{name}]:", "yellow", attrs=["bold"]))
101
+
102
+ resp = get_llm_response(
103
+ prompt,
104
+ model=model,
105
+ provider=provider,
106
+ npc=participant.get('npc') or npc,
107
+ temperature=0.7
108
+ )
109
+
110
+ contribution = str(resp.get('response', ''))
111
+ print(contribution)
112
+
113
+ entry = {
114
+ 'round': round_num,
115
+ 'speaker': name,
116
+ 'contribution': contribution
117
+ }
118
+ round_contributions.append(entry)
119
+ discussion_log.append(entry)
120
+
121
+ # Sample a followup from another participant
122
+ other_participants = [p for p in participants if p['name'] != name]
123
+ if other_participants:
124
+ followup_participant = random.choice(other_participants)
125
+ followup_name = followup_participant['name']
126
+ followup_persona = followup_participant['persona']
127
+
128
+ followup_prompt = f"""You are {followup_name}. {followup_persona}
129
+
130
+ Topic: "{topic}"
131
+
132
+ {name} just said: "{contribution[:500]}"
133
+
134
+ Respond briefly to this specific point - agree, disagree, build on it, or ask a clarifying question.
135
+ Keep it to 2-3 sentences.
136
+ """
137
+
138
+ print(colored(f"\n [{followup_name} responds]:", "cyan"))
139
+
140
+ followup_resp = get_llm_response(
141
+ followup_prompt,
142
+ model=model,
143
+ provider=provider,
144
+ npc=followup_participant.get('npc') or npc,
145
+ temperature=0.7
146
+ )
147
+
148
+ followup_contribution = str(followup_resp.get('response', ''))
149
+ print(f" {followup_contribution}")
150
+
151
+ discussion_log.append({
152
+ 'round': round_num,
153
+ 'speaker': followup_name,
154
+ 'contribution': followup_contribution,
155
+ 'type': 'followup'
156
+ })
157
+
158
+ # Probability of original speaker responding back vs someone else
159
+ if random.random() < 0.4:
160
+ # Original speaker responds
161
+ responder = participant
162
+ responder_name = name
163
+ else:
164
+ # Sample from others (could be followup person or someone else)
165
+ responder = random.choice(other_participants)
166
+ responder_name = responder['name']
167
+
168
+ if random.random() < 0.6: # 60% chance of a counter-response
169
+ counter_prompt = f"""You are {responder_name}. {responder['persona']}
170
+
171
+ Topic: "{topic}"
172
+
173
+ {followup_name} responded: "{followup_contribution}"
174
+
175
+ Brief reaction (1-2 sentences). Move the discussion forward.
176
+ """
177
+
178
+ print(colored(f"\n [{responder_name}]:", "magenta"))
179
+
180
+ counter_resp = get_llm_response(
181
+ counter_prompt,
182
+ model=model,
183
+ provider=provider,
184
+ npc=responder.get('npc') or npc,
185
+ temperature=0.7
186
+ )
187
+
188
+ counter_contribution = str(counter_resp.get('response', ''))
189
+ print(f" {counter_contribution}")
190
+
191
+ discussion_log.append({
192
+ 'round': round_num,
193
+ 'speaker': responder_name,
194
+ 'contribution': counter_contribution,
195
+ 'type': 'counter'
196
+ })
197
+
198
+ # Synthesis
199
+ print(colored(f"\n{'='*60}", "green"))
200
+ print(colored(" SYNTHESIS", "green", attrs=["bold"]))
201
+ print(colored(f"{'='*60}", "green"))
202
+
203
+ all_contributions = "\n".join([
204
+ f"[{e['speaker']} - Round {e['round']}]: {e['contribution']}"
205
+ for e in discussion_log
206
+ ])
207
+
208
+ synthesis_prompt = f"""As the convener of this discussion on "{topic}", synthesize the key points:
209
+
210
+ Full discussion:
211
+ {all_contributions}
212
+
213
+ Provide:
214
+ 1. Key agreements and consensus points
215
+ 2. Areas of disagreement or tension
216
+ 3. Novel ideas that emerged
217
+ 4. Recommended next steps or actions
218
+ """
219
+
220
+ resp = get_llm_response(synthesis_prompt, model=model, provider=provider, npc=npc, temperature=0.4)
221
+ synthesis = str(resp.get('response', ''))
222
+ print(synthesis)
223
+
224
+ context['output'] = synthesis
225
+ context['messages'] = messages
226
+ context['convene_result'] = {
227
+ 'topic': topic,
228
+ 'participants': npc_names,
229
+ 'rounds': rounds,
230
+ 'discussion': discussion_log,
231
+ 'synthesis': synthesis
232
+ }
@@ -0,0 +1,31 @@
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"
13
+ primary_directive: |
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
31
+