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,230 @@
1
+ jinx_name: "sync"
2
+ description: "Sync npc_team files from the npcsh repo to ~/.npcsh/npc_team. Detects local modifications before overwriting."
3
+ inputs:
4
+ - force: "" # Use --force or -f to overwrite all files without prompting
5
+ - dry_run: "" # Use --dry-run or -d to preview changes without applying them
6
+ - jinxs: "" # Use --jinxs to sync only .jinx files
7
+ - npcs: "" # Use --npcs to sync only .npc files
8
+ - ctx: "" # Use --ctx to sync only .ctx files
9
+ - images: "" # Use --images to sync only image files (.png, .jpg, .jpeg)
10
+ steps:
11
+ - name: "sync_npc_team"
12
+ engine: "python"
13
+ code: |
14
+ import os
15
+ import hashlib
16
+ import shutil
17
+ from pathlib import Path
18
+ from datetime import datetime
19
+
20
+ force = context.get('force', False)
21
+ dry_run = context.get('dry_run', False)
22
+ sync_jinxs = context.get('jinxs', False)
23
+ sync_npcs = context.get('npcs', False)
24
+ sync_ctx = context.get('ctx', False)
25
+ sync_images = context.get('images', False)
26
+
27
+ # Convert string flags to boolean
28
+ def to_bool(val):
29
+ if isinstance(val, bool):
30
+ return val
31
+ if isinstance(val, str):
32
+ return val.lower() in ('true', '1', 'yes', 'y')
33
+ return bool(val)
34
+
35
+ force = to_bool(force)
36
+ dry_run = to_bool(dry_run)
37
+ sync_jinxs = to_bool(sync_jinxs)
38
+ sync_npcs = to_bool(sync_npcs)
39
+ sync_ctx = to_bool(sync_ctx)
40
+ sync_images = to_bool(sync_images)
41
+
42
+ # If none specified, sync all
43
+ sync_all = not (sync_jinxs or sync_npcs or sync_ctx or sync_images)
44
+
45
+ # Find the repo npc_team directory
46
+ # Check common locations
47
+ possible_repo_paths = [
48
+ Path.home() / "npcww" / "npc-core" / "npcsh" / "npcsh" / "npc_team",
49
+ Path.home() / "npc-core" / "npcsh" / "npcsh" / "npc_team",
50
+ Path.home() / "repos" / "npcsh" / "npcsh" / "npc_team",
51
+ Path.home() / "Projects" / "npcsh" / "npcsh" / "npc_team",
52
+ ]
53
+
54
+ # Also check if we can find it via pip show
55
+ try:
56
+ import subprocess
57
+ result = subprocess.run(['pip', 'show', 'npcsh', '-f'], capture_output=True, text=True)
58
+ if result.returncode == 0:
59
+ for line in result.stdout.split('\n'):
60
+ if line.startswith('Location:'):
61
+ pip_path = Path(line.split(':', 1)[1].strip()) / "npcsh" / "npc_team"
62
+ if pip_path.exists():
63
+ possible_repo_paths.insert(0, pip_path)
64
+ except:
65
+ pass
66
+
67
+ repo_npc_team = None
68
+ for path in possible_repo_paths:
69
+ if path.exists() and path.is_dir():
70
+ repo_npc_team = path
71
+ break
72
+
73
+ local_npc_team = Path.home() / ".npcsh" / "npc_team"
74
+
75
+ if not repo_npc_team:
76
+ context['output'] = "Error: Could not find npcsh repo npc_team directory.\nSearched:\n" + "\n".join(f" - {p}" for p in possible_repo_paths)
77
+ exit()
78
+
79
+ if not local_npc_team.exists():
80
+ context['output'] = f"Error: Local npc_team directory not found at {local_npc_team}"
81
+ exit()
82
+
83
+ def get_file_hash(filepath):
84
+ """Get MD5 hash of file contents."""
85
+ try:
86
+ with open(filepath, 'rb') as f:
87
+ return hashlib.md5(f.read()).hexdigest()
88
+ except:
89
+ return None
90
+
91
+ def get_files_recursive(base_path, extensions=None):
92
+ """Get all files recursively, optionally filtered by extensions."""
93
+ files = []
94
+ for root, dirs, filenames in os.walk(base_path):
95
+ # Skip .git directories
96
+ dirs[:] = [d for d in dirs if d != '.git']
97
+ for filename in filenames:
98
+ if filename.startswith('.'):
99
+ continue
100
+ if extensions and not any(filename.endswith(ext) for ext in extensions):
101
+ continue
102
+ full_path = Path(root) / filename
103
+ rel_path = full_path.relative_to(base_path)
104
+ files.append(rel_path)
105
+ return files
106
+
107
+ # Build list of extensions to sync based on flags
108
+ sync_extensions = []
109
+ if sync_all or sync_npcs:
110
+ sync_extensions.append('.npc')
111
+ if sync_all or sync_ctx:
112
+ sync_extensions.append('.ctx')
113
+ if sync_all or sync_jinxs:
114
+ sync_extensions.append('.jinx')
115
+ if sync_all or sync_images:
116
+ sync_extensions.extend(['.png', '.jpg', '.jpeg'])
117
+
118
+ # Get files from repo
119
+ repo_files = get_files_recursive(repo_npc_team, sync_extensions)
120
+
121
+ output_lines = []
122
+ output_lines.append(f"Syncing from: {repo_npc_team}")
123
+ output_lines.append(f"Syncing to: {local_npc_team}")
124
+
125
+ # Show what's being synced
126
+ sync_types = []
127
+ if sync_all:
128
+ sync_types.append("all")
129
+ else:
130
+ if sync_npcs: sync_types.append("npcs")
131
+ if sync_ctx: sync_types.append("ctx")
132
+ if sync_jinxs: sync_types.append("jinxs")
133
+ if sync_images: sync_types.append("images")
134
+ output_lines.append(f"Syncing: {', '.join(sync_types)}")
135
+
136
+ if dry_run:
137
+ output_lines.append("\n[DRY RUN - No changes will be made]\n")
138
+ output_lines.append("")
139
+
140
+ new_files = []
141
+ updated_files = []
142
+ modified_locally = []
143
+ unchanged_files = []
144
+
145
+ for rel_path in repo_files:
146
+ repo_file = repo_npc_team / rel_path
147
+ local_file = local_npc_team / rel_path
148
+
149
+ if not local_file.exists():
150
+ new_files.append(rel_path)
151
+ else:
152
+ repo_hash = get_file_hash(repo_file)
153
+ local_hash = get_file_hash(local_file)
154
+
155
+ if repo_hash == local_hash:
156
+ unchanged_files.append(rel_path)
157
+ else:
158
+ # Check if local file is newer (possibly modified by user)
159
+ repo_mtime = repo_file.stat().st_mtime
160
+ local_mtime = local_file.stat().st_mtime
161
+
162
+ if local_mtime > repo_mtime:
163
+ modified_locally.append((rel_path, local_mtime, repo_mtime))
164
+ else:
165
+ updated_files.append(rel_path)
166
+
167
+ # Report findings
168
+ if new_files:
169
+ output_lines.append(f"New files to add ({len(new_files)}):")
170
+ for f in new_files:
171
+ output_lines.append(f" + {f}")
172
+ output_lines.append("")
173
+
174
+ if updated_files:
175
+ output_lines.append(f"Files to update ({len(updated_files)}):")
176
+ for f in updated_files:
177
+ output_lines.append(f" ~ {f}")
178
+ output_lines.append("")
179
+
180
+ if modified_locally:
181
+ output_lines.append(f"Locally modified files ({len(modified_locally)}):")
182
+ for f, local_t, repo_t in modified_locally:
183
+ local_dt = datetime.fromtimestamp(local_t).strftime('%Y-%m-%d %H:%M')
184
+ repo_dt = datetime.fromtimestamp(repo_t).strftime('%Y-%m-%d %H:%M')
185
+ output_lines.append(f" ! {f}")
186
+ output_lines.append(f" local: {local_dt} repo: {repo_dt}")
187
+ if not force:
188
+ output_lines.append(" (use --force to overwrite these)")
189
+ output_lines.append("")
190
+
191
+ if unchanged_files:
192
+ output_lines.append(f"Already up to date: {len(unchanged_files)} files")
193
+ output_lines.append("")
194
+
195
+ # Perform sync if not dry run
196
+ if not dry_run:
197
+ synced = 0
198
+ skipped = 0
199
+
200
+ # Sync new files
201
+ for rel_path in new_files:
202
+ src = repo_npc_team / rel_path
203
+ dst = local_npc_team / rel_path
204
+ dst.parent.mkdir(parents=True, exist_ok=True)
205
+ shutil.copy2(src, dst)
206
+ synced += 1
207
+
208
+ # Sync updated files
209
+ for rel_path in updated_files:
210
+ src = repo_npc_team / rel_path
211
+ dst = local_npc_team / rel_path
212
+ dst.parent.mkdir(parents=True, exist_ok=True)
213
+ shutil.copy2(src, dst)
214
+ synced += 1
215
+
216
+ # Handle locally modified files
217
+ for rel_path, _, _ in modified_locally:
218
+ if force:
219
+ src = repo_npc_team / rel_path
220
+ dst = local_npc_team / rel_path
221
+ shutil.copy2(src, dst)
222
+ synced += 1
223
+ else:
224
+ skipped += 1
225
+
226
+ output_lines.append(f"Synced: {synced} files")
227
+ if skipped:
228
+ output_lines.append(f"Skipped: {skipped} locally modified files")
229
+
230
+ context['output'] = "\n".join(output_lines)
@@ -0,0 +1,205 @@
1
+ jinx_name: teamviz
2
+ description: "Visualize NPC team structure - NPCs, jinxs, and their relationships"
3
+ inputs:
4
+ - team_path: ""
5
+ - save: ""
6
+
7
+ steps:
8
+ - name: visualize_team
9
+ engine: python
10
+ code: |
11
+ import os
12
+ import yaml
13
+ from pathlib import Path
14
+ import matplotlib.pyplot as plt
15
+ import networkx as nx
16
+ from collections import defaultdict
17
+
18
+ team_path = context.get('team_path') or ''
19
+ save_path = context.get('save') or ''
20
+
21
+ # Find team path
22
+ if not team_path:
23
+ if os.path.exists('./npc_team'):
24
+ team_path = './npc_team'
25
+ elif os.path.exists(os.path.expanduser('~/.npcsh/npc_team')):
26
+ team_path = os.path.expanduser('~/.npcsh/npc_team')
27
+ else:
28
+ output = "No npc_team found. Specify team_path."
29
+ exit()
30
+
31
+ team_path = Path(team_path)
32
+
33
+ # Load NPCs
34
+ npcs = {}
35
+ for npc_file in team_path.glob("*.npc"):
36
+ try:
37
+ with open(npc_file, 'r') as f:
38
+ data = yaml.safe_load(f)
39
+ npcs[npc_file.stem] = {
40
+ 'jinxs': data.get('jinxs', []),
41
+ 'directive': data.get('primary_directive', '')[:100],
42
+ 'model': data.get('model', ''),
43
+ }
44
+ except Exception as e:
45
+ print("Error loading {}: {}".format(npc_file, e))
46
+
47
+ # Load team context
48
+ team_name = "NPC Team"
49
+ forenpc = None
50
+ for ctx_file in team_path.glob("*.ctx"):
51
+ try:
52
+ with open(ctx_file, 'r') as f:
53
+ ctx = yaml.safe_load(f)
54
+ team_name = ctx.get('name', team_name)
55
+ forenpc = ctx.get('forenpc')
56
+ except:
57
+ pass
58
+
59
+ # Discover all jinxs
60
+ jinxs_dir = team_path / "jinxs"
61
+ all_jinxs = {}
62
+ if jinxs_dir.exists():
63
+ for jinx_file in jinxs_dir.rglob("*.jinx"):
64
+ rel_path = jinx_file.relative_to(jinxs_dir)
65
+ jinx_name = jinx_file.stem
66
+ # Categorize by location
67
+ if 'bin' in str(rel_path):
68
+ category = 'bin'
69
+ elif 'lib' in str(rel_path):
70
+ parts = str(rel_path).split(os.sep)
71
+ category = parts[1] if len(parts) > 1 else 'lib'
72
+ else:
73
+ category = 'other'
74
+ all_jinxs[jinx_name] = {'path': str(rel_path), 'category': category}
75
+
76
+ # Build graph
77
+ G = nx.DiGraph()
78
+
79
+ # Add team node
80
+ G.add_node(team_name, type='team', size=3000)
81
+
82
+ # Add NPC nodes
83
+ for npc_name, npc_data in npcs.items():
84
+ node_type = 'forenpc' if npc_name == forenpc else 'npc'
85
+ G.add_node(npc_name, type=node_type, size=2000)
86
+ G.add_edge(team_name, npc_name, relation='has_npc')
87
+
88
+ # Track jinx usage
89
+ jinx_users = defaultdict(list)
90
+ npc_jinx_patterns = {}
91
+
92
+ for npc_name, npc_data in npcs.items():
93
+ patterns = npc_data.get('jinxs', [])
94
+ npc_jinx_patterns[npc_name] = patterns
95
+
96
+ # Resolve patterns to actual jinxs
97
+ for pattern in patterns:
98
+ pattern = str(pattern)
99
+ if '*' in pattern:
100
+ # Glob pattern like lib/browser/*
101
+ base = pattern.replace('/*', '').replace('*', '')
102
+ for jname, jdata in all_jinxs.items():
103
+ if base in jdata['path']:
104
+ jinx_users[jname].append(npc_name)
105
+ else:
106
+ # Exact match
107
+ jname = pattern.split('/')[-1]
108
+ if jname in all_jinxs:
109
+ jinx_users[jname].append(npc_name)
110
+
111
+ # Add jinx nodes (only ones that are used)
112
+ jinx_categories = defaultdict(list)
113
+ for jname, users in jinx_users.items():
114
+ if jname in all_jinxs:
115
+ cat = all_jinxs[jname]['category']
116
+ jinx_categories[cat].append(jname)
117
+
118
+ # Size based on usage
119
+ size = 500 + len(users) * 200
120
+ G.add_node(jname, type='jinx', category=cat, size=size, users=len(users))
121
+
122
+ for user in users:
123
+ G.add_edge(user, jname, relation='uses')
124
+
125
+ # Check for delegation relationships
126
+ for npc_name, npc_data in npcs.items():
127
+ directive = npc_data.get('directive', '').lower()
128
+ for other_npc in npcs.keys():
129
+ if other_npc != npc_name and other_npc in directive:
130
+ if 'delegate' in directive:
131
+ G.add_edge(npc_name, other_npc, relation='delegates_to')
132
+
133
+ # Create visualization
134
+ fig, ax = plt.subplots(1, 1, figsize=(16, 12))
135
+
136
+ # Layout
137
+ pos = nx.spring_layout(G, k=2, iterations=50, seed=42)
138
+
139
+ # Color map
140
+ color_map = {
141
+ 'team': '#FF6B6B',
142
+ 'forenpc': '#4ECDC4',
143
+ 'npc': '#45B7D1',
144
+ 'jinx': '#96CEB4',
145
+ }
146
+
147
+ # Draw by type
148
+ for node_type in ['team', 'forenpc', 'npc', 'jinx']:
149
+ nodes = [n for n, d in G.nodes(data=True) if d.get('type') == node_type]
150
+ sizes = [G.nodes[n].get('size', 1000) for n in nodes]
151
+ nx.draw_networkx_nodes(G, pos, nodelist=nodes, node_color=color_map[node_type],
152
+ node_size=sizes, alpha=0.9, ax=ax)
153
+
154
+ # Draw edges by type
155
+ delegate_edges = [(u, v) for u, v, d in G.edges(data=True) if d.get('relation') == 'delegates_to']
156
+ use_edges = [(u, v) for u, v, d in G.edges(data=True) if d.get('relation') == 'uses']
157
+ has_edges = [(u, v) for u, v, d in G.edges(data=True) if d.get('relation') == 'has_npc']
158
+
159
+ nx.draw_networkx_edges(G, pos, edgelist=has_edges, edge_color='#999', alpha=0.5, ax=ax)
160
+ nx.draw_networkx_edges(G, pos, edgelist=use_edges, edge_color='#96CEB4', alpha=0.3, ax=ax)
161
+ nx.draw_networkx_edges(G, pos, edgelist=delegate_edges, edge_color='#FF6B6B',
162
+ width=2, style='dashed', ax=ax, arrows=True, arrowsize=20)
163
+
164
+ # Labels
165
+ nx.draw_networkx_labels(G, pos, font_size=8, font_weight='bold', ax=ax)
166
+
167
+ # Legend
168
+ legend_elements = [
169
+ plt.scatter([], [], c=color_map['team'], s=200, label='Team'),
170
+ plt.scatter([], [], c=color_map['forenpc'], s=200, label='ForeNPC'),
171
+ plt.scatter([], [], c=color_map['npc'], s=200, label='NPC'),
172
+ plt.scatter([], [], c=color_map['jinx'], s=200, label='Jinx'),
173
+ ]
174
+ ax.legend(handles=legend_elements, loc='upper left')
175
+
176
+ ax.set_title(team_name + " - Team Structure", fontsize=14, fontweight='bold')
177
+ ax.axis('off')
178
+
179
+ plt.tight_layout()
180
+
181
+ if save_path:
182
+ plt.savefig(save_path, dpi=150, bbox_inches='tight')
183
+ plt.close()
184
+ output = "Saved to " + save_path
185
+ else:
186
+ plt.show()
187
+ output = "Displayed team visualization"
188
+
189
+ # Text summary
190
+ summary = "\n\n--- Team Summary ---\n"
191
+ summary += "Team: {}\n".format(team_name)
192
+ summary += "ForeNPC: {}\n".format(forenpc or 'None')
193
+ summary += "NPCs ({}): {}\n".format(len(npcs), ', '.join(sorted(npcs.keys())))
194
+ summary += "Total Jinxs: {}\n".format(len(all_jinxs))
195
+ summary += "\nJinxs by category:\n"
196
+ for cat, jlist in sorted(jinx_categories.items()):
197
+ summary += " {}: {} jinxs\n".format(cat, len(jlist))
198
+
199
+ summary += "\nMost shared jinxs:\n"
200
+ shared = sorted(jinx_users.items(), key=lambda x: len(x[1]), reverse=True)[:10]
201
+ for jname, users in shared:
202
+ if len(users) > 1:
203
+ summary += " {} (used by {}): {}\n".format(jname, len(users), ', '.join(users))
204
+
205
+ output = output + summary
@@ -0,0 +1,27 @@
1
+ jinx_name: type_text
2
+ description: Type text using keyboard
3
+ inputs:
4
+ - text: "" # Text to type
5
+
6
+ steps:
7
+ - name: perform_type
8
+ engine: python
9
+ code: |
10
+ from npcpy.work.desktop import perform_action
11
+
12
+ text = context.get('text', '')
13
+ messages = context.get('messages', [])
14
+
15
+ if not text:
16
+ context['output'] = "Usage: /type_text <text to type>"
17
+ context['messages'] = messages
18
+ exit()
19
+
20
+ try:
21
+ perform_action({'type': 'type', 'text': text})
22
+ preview = text[:30] + "..." if len(text) > 30 else text
23
+ context['output'] = f"Typed: {preview}"
24
+ except Exception as e:
25
+ context['output'] = f"Type failed: {e}"
26
+
27
+ context['messages'] = messages
@@ -0,0 +1,17 @@
1
+ jinx_name: "verbose"
2
+ description: "Enable verbose logging - shows debug output from npcpy"
3
+ inputs: []
4
+ steps:
5
+ - name: "set_verbose"
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("verbose")
13
+ context['output'] = result
14
+ else:
15
+ context['output'] = "Error: state not available"
16
+
17
+ context['messages'] = output_messages
@@ -41,27 +41,15 @@ steps:
41
41
  except (ValueError, TypeError):
42
42
  width = 1024
43
43
 
44
- # Get model and provider, prioritizing context, then NPC, then environment variables
44
+ # Get model and provider from context or environment
45
45
  model = context.get('model')
46
46
  provider = context.get('provider')
47
-
48
- # Use NPC's model/provider as fallback
49
- if not model and npc and hasattr(npc, 'model') and npc.model:
50
- model = npc.model
51
- if not provider and npc and hasattr(npc, 'provider') and npc.provider:
52
- provider = npc.provider
53
-
47
+
54
48
  # Fallback to environment variables
55
49
  if not model:
56
50
  model = os.getenv('NPCSH_IMAGE_GEN_MODEL')
57
51
  if not provider:
58
52
  provider = os.getenv('NPCSH_IMAGE_GEN_PROVIDER')
59
-
60
- # Final hardcoded fallbacks if nothing else is set
61
- if not model:
62
- model = "runwayml/stable-diffusion-v1-5"
63
- if not provider:
64
- provider = "diffusers"
65
53
 
66
54
  # Parse attachments
67
55
  input_images = []
@@ -93,12 +81,11 @@ steps:
93
81
  images_list = result
94
82
 
95
83
  saved_files = []
96
- html_image_tags = [] # This list will store the raw HTML <img> tags
97
-
84
+
98
85
  for i, image in enumerate(images_list):
99
86
  if image is None:
100
87
  continue
101
-
88
+
102
89
  # Determine output filename
103
90
  if output_name and str(output_name).strip():
104
91
  base_name, ext = os.path.splitext(os.path.expanduser(str(output_name)))
@@ -111,25 +98,16 @@ steps:
111
98
  os.path.expanduser("~/.npcsh/images/")
112
99
  + f"image_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{i}.png"
113
100
  )
114
-
101
+
115
102
  # Save image to file
116
103
  image.save(current_output_file)
117
104
  saved_files.append(current_output_file)
118
105
 
119
- # Convert image to base64 and create an HTML <img> tag
120
- with open(current_output_file, 'rb') as f:
121
- img_data = base64.b64encode(f.read()).decode()
122
- # Using raw HTML <img> tag with data URI
123
- html_image_tags.append(f'<img src="data:image/png;base64,{img_data}" alt="Generated Image {i+1}" style="max-width: 100%; display: block; margin-top: 10px;">')
124
-
125
106
  if saved_files:
126
- output_text_message = f"Image(s) generated and saved to: {', '.join(saved_files)}"
107
+ output = f"Image(s) generated: {', '.join(saved_files)}"
127
108
  if input_images:
128
- output_text_message = f"Image(s) edited and saved to: {', '.join(saved_files)}"
129
-
130
- output = output_text_message # Keep the text message clean
131
- output += f"\n\nThe image files have been saved and are ready to view."
132
- output += "\n\n" + "\n".join(html_image_tags) # Append all HTML <img> tags to the output
109
+ output = f"Image(s) edited: {', '.join(saved_files)}"
110
+ context['generated_images'] = saved_files
133
111
  else:
134
112
  output = "No images were generated."
135
113
 
@@ -0,0 +1,21 @@
1
+ jinx_name: wait
2
+ description: Wait/pause for a specified duration in seconds
3
+ inputs:
4
+ - duration: 1 # Duration to wait in seconds
5
+
6
+ steps:
7
+ - name: perform_wait
8
+ engine: python
9
+ code: |
10
+ import time
11
+
12
+ duration = float(context.get('duration', 1))
13
+ messages = context.get('messages', [])
14
+
15
+ try:
16
+ time.sleep(duration)
17
+ context['output'] = f"Waited {duration} seconds"
18
+ except Exception as e:
19
+ context['output'] = f"Wait failed: {e}"
20
+
21
+ context['messages'] = messages