synapse-sdk 1.0.0a23__py3-none-any.whl → 2025.12.3__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 (228) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/__init__.py +310 -5
  3. synapse_sdk/cli/alias/__init__.py +22 -0
  4. synapse_sdk/cli/alias/create.py +36 -0
  5. synapse_sdk/cli/alias/dataclass.py +31 -0
  6. synapse_sdk/cli/alias/default.py +16 -0
  7. synapse_sdk/cli/alias/delete.py +15 -0
  8. synapse_sdk/cli/alias/list.py +19 -0
  9. synapse_sdk/cli/alias/read.py +15 -0
  10. synapse_sdk/cli/alias/update.py +17 -0
  11. synapse_sdk/cli/alias/utils.py +61 -0
  12. synapse_sdk/cli/code_server.py +687 -0
  13. synapse_sdk/cli/config.py +440 -0
  14. synapse_sdk/cli/devtools.py +90 -0
  15. synapse_sdk/cli/plugin/__init__.py +33 -0
  16. synapse_sdk/cli/{create_plugin.py → plugin/create.py} +2 -2
  17. synapse_sdk/{plugins/cli → cli/plugin}/publish.py +23 -15
  18. synapse_sdk/clients/agent/__init__.py +9 -3
  19. synapse_sdk/clients/agent/container.py +143 -0
  20. synapse_sdk/clients/agent/core.py +19 -0
  21. synapse_sdk/clients/agent/ray.py +298 -9
  22. synapse_sdk/clients/backend/__init__.py +30 -12
  23. synapse_sdk/clients/backend/annotation.py +13 -5
  24. synapse_sdk/clients/backend/core.py +31 -4
  25. synapse_sdk/clients/backend/data_collection.py +186 -0
  26. synapse_sdk/clients/backend/hitl.py +17 -0
  27. synapse_sdk/clients/backend/integration.py +16 -1
  28. synapse_sdk/clients/backend/ml.py +5 -1
  29. synapse_sdk/clients/backend/models.py +78 -0
  30. synapse_sdk/clients/base.py +384 -41
  31. synapse_sdk/clients/ray/serve.py +2 -0
  32. synapse_sdk/clients/validators/collections.py +31 -0
  33. synapse_sdk/devtools/config.py +94 -0
  34. synapse_sdk/devtools/server.py +41 -0
  35. synapse_sdk/devtools/streamlit_app/__init__.py +5 -0
  36. synapse_sdk/devtools/streamlit_app/app.py +128 -0
  37. synapse_sdk/devtools/streamlit_app/services/__init__.py +11 -0
  38. synapse_sdk/devtools/streamlit_app/services/job_service.py +233 -0
  39. synapse_sdk/devtools/streamlit_app/services/plugin_service.py +236 -0
  40. synapse_sdk/devtools/streamlit_app/services/serve_service.py +95 -0
  41. synapse_sdk/devtools/streamlit_app/ui/__init__.py +15 -0
  42. synapse_sdk/devtools/streamlit_app/ui/config_tab.py +76 -0
  43. synapse_sdk/devtools/streamlit_app/ui/deployment_tab.py +66 -0
  44. synapse_sdk/devtools/streamlit_app/ui/http_tab.py +125 -0
  45. synapse_sdk/devtools/streamlit_app/ui/jobs_tab.py +573 -0
  46. synapse_sdk/devtools/streamlit_app/ui/serve_tab.py +346 -0
  47. synapse_sdk/devtools/streamlit_app/ui/status_bar.py +118 -0
  48. synapse_sdk/devtools/streamlit_app/utils/__init__.py +40 -0
  49. synapse_sdk/devtools/streamlit_app/utils/json_viewer.py +197 -0
  50. synapse_sdk/devtools/streamlit_app/utils/log_formatter.py +38 -0
  51. synapse_sdk/devtools/streamlit_app/utils/styles.py +241 -0
  52. synapse_sdk/devtools/streamlit_app/utils/ui_components.py +289 -0
  53. synapse_sdk/devtools/streamlit_app.py +10 -0
  54. synapse_sdk/loggers.py +120 -9
  55. synapse_sdk/plugins/README.md +1340 -0
  56. synapse_sdk/plugins/__init__.py +0 -13
  57. synapse_sdk/plugins/categories/base.py +117 -11
  58. synapse_sdk/plugins/categories/data_validation/actions/validation.py +72 -0
  59. synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +33 -5
  60. synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
  61. synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
  62. synapse_sdk/plugins/categories/export/actions/export/action.py +165 -0
  63. synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
  64. synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
  65. synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
  66. synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
  67. synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
  68. synapse_sdk/plugins/categories/export/templates/config.yaml +21 -0
  69. synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +390 -0
  70. synapse_sdk/plugins/categories/export/templates/plugin/export.py +160 -0
  71. synapse_sdk/plugins/categories/neural_net/actions/deployment.py +13 -12
  72. synapse_sdk/plugins/categories/neural_net/actions/train.py +1134 -31
  73. synapse_sdk/plugins/categories/neural_net/actions/tune.py +534 -0
  74. synapse_sdk/plugins/categories/neural_net/base/inference.py +1 -1
  75. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +32 -4
  76. synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +26 -10
  77. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  78. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  79. synapse_sdk/plugins/categories/{export/actions/export.py → pre_annotation/actions/pre_annotation/action.py} +4 -4
  80. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  81. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +148 -0
  82. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  83. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  84. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  85. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +100 -0
  86. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +248 -0
  87. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  88. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  89. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +265 -0
  90. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  91. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  92. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +92 -0
  93. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +243 -0
  94. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  95. synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +19 -0
  96. synapse_sdk/plugins/categories/pre_annotation/templates/plugin/to_task.py +40 -0
  97. synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +2 -0
  98. synapse_sdk/plugins/categories/upload/__init__.py +0 -0
  99. synapse_sdk/plugins/categories/upload/actions/__init__.py +0 -0
  100. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +19 -0
  101. synapse_sdk/plugins/categories/upload/actions/upload/action.py +236 -0
  102. synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
  103. synapse_sdk/plugins/categories/upload/actions/upload/enums.py +493 -0
  104. synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py +36 -0
  105. synapse_sdk/plugins/categories/upload/actions/upload/factory.py +138 -0
  106. synapse_sdk/plugins/categories/upload/actions/upload/models.py +214 -0
  107. synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +183 -0
  108. synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
  109. synapse_sdk/plugins/categories/upload/actions/upload/run.py +179 -0
  110. synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
  111. synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +107 -0
  112. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
  113. synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +63 -0
  114. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +91 -0
  115. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +82 -0
  116. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +235 -0
  117. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +201 -0
  118. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +104 -0
  119. synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +71 -0
  120. synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
  121. synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +82 -0
  122. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
  123. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
  124. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +29 -0
  125. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
  126. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +300 -0
  127. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +287 -0
  128. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
  129. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
  130. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
  131. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
  132. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +84 -0
  133. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
  134. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +60 -0
  135. synapse_sdk/plugins/categories/upload/actions/upload/utils.py +250 -0
  136. synapse_sdk/plugins/categories/upload/templates/README.md +470 -0
  137. synapse_sdk/plugins/categories/upload/templates/config.yaml +33 -0
  138. synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +310 -0
  139. synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +102 -0
  140. synapse_sdk/plugins/enums.py +3 -1
  141. synapse_sdk/plugins/models.py +148 -11
  142. synapse_sdk/plugins/templates/plugin-config-schema.json +406 -0
  143. synapse_sdk/plugins/templates/schema.json +491 -0
  144. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +1 -0
  145. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +1 -1
  146. synapse_sdk/plugins/utils/__init__.py +46 -0
  147. synapse_sdk/plugins/utils/actions.py +119 -0
  148. synapse_sdk/plugins/utils/config.py +203 -0
  149. synapse_sdk/plugins/{utils.py → utils/legacy.py} +26 -46
  150. synapse_sdk/plugins/utils/ray_gcs.py +66 -0
  151. synapse_sdk/plugins/utils/registry.py +58 -0
  152. synapse_sdk/shared/__init__.py +25 -0
  153. synapse_sdk/shared/enums.py +93 -0
  154. synapse_sdk/types.py +19 -0
  155. synapse_sdk/utils/converters/__init__.py +240 -0
  156. synapse_sdk/utils/converters/coco/__init__.py +0 -0
  157. synapse_sdk/utils/converters/coco/from_dm.py +322 -0
  158. synapse_sdk/utils/converters/coco/to_dm.py +215 -0
  159. synapse_sdk/utils/converters/dm/__init__.py +57 -0
  160. synapse_sdk/utils/converters/dm/base.py +137 -0
  161. synapse_sdk/utils/converters/dm/from_v1.py +273 -0
  162. synapse_sdk/utils/converters/dm/to_v1.py +321 -0
  163. synapse_sdk/utils/converters/dm/tools/__init__.py +214 -0
  164. synapse_sdk/utils/converters/dm/tools/answer.py +95 -0
  165. synapse_sdk/utils/converters/dm/tools/bounding_box.py +132 -0
  166. synapse_sdk/utils/converters/dm/tools/bounding_box_3d.py +121 -0
  167. synapse_sdk/utils/converters/dm/tools/classification.py +75 -0
  168. synapse_sdk/utils/converters/dm/tools/keypoint.py +117 -0
  169. synapse_sdk/utils/converters/dm/tools/named_entity.py +111 -0
  170. synapse_sdk/utils/converters/dm/tools/polygon.py +122 -0
  171. synapse_sdk/utils/converters/dm/tools/polyline.py +124 -0
  172. synapse_sdk/utils/converters/dm/tools/prompt.py +94 -0
  173. synapse_sdk/utils/converters/dm/tools/relation.py +86 -0
  174. synapse_sdk/utils/converters/dm/tools/segmentation.py +141 -0
  175. synapse_sdk/utils/converters/dm/tools/segmentation_3d.py +83 -0
  176. synapse_sdk/utils/converters/dm/types.py +168 -0
  177. synapse_sdk/utils/converters/dm/utils.py +162 -0
  178. synapse_sdk/utils/converters/dm_legacy/__init__.py +56 -0
  179. synapse_sdk/utils/converters/dm_legacy/from_v1.py +627 -0
  180. synapse_sdk/utils/converters/dm_legacy/to_v1.py +367 -0
  181. synapse_sdk/utils/converters/pascal/__init__.py +0 -0
  182. synapse_sdk/utils/converters/pascal/from_dm.py +244 -0
  183. synapse_sdk/utils/converters/pascal/to_dm.py +214 -0
  184. synapse_sdk/utils/converters/yolo/__init__.py +0 -0
  185. synapse_sdk/utils/converters/yolo/from_dm.py +384 -0
  186. synapse_sdk/utils/converters/yolo/to_dm.py +267 -0
  187. synapse_sdk/utils/dataset.py +46 -0
  188. synapse_sdk/utils/encryption.py +158 -0
  189. synapse_sdk/utils/file/__init__.py +58 -0
  190. synapse_sdk/utils/file/archive.py +32 -0
  191. synapse_sdk/utils/file/checksum.py +56 -0
  192. synapse_sdk/utils/file/chunking.py +31 -0
  193. synapse_sdk/utils/file/download.py +385 -0
  194. synapse_sdk/utils/file/encoding.py +40 -0
  195. synapse_sdk/utils/file/io.py +22 -0
  196. synapse_sdk/utils/file/upload.py +165 -0
  197. synapse_sdk/utils/file/video/__init__.py +29 -0
  198. synapse_sdk/utils/file/video/transcode.py +307 -0
  199. synapse_sdk/utils/file.py.backup +301 -0
  200. synapse_sdk/utils/http.py +138 -0
  201. synapse_sdk/utils/network.py +309 -0
  202. synapse_sdk/utils/storage/__init__.py +72 -0
  203. synapse_sdk/utils/storage/providers/__init__.py +183 -0
  204. synapse_sdk/utils/storage/providers/file_system.py +134 -0
  205. synapse_sdk/utils/storage/providers/gcp.py +13 -0
  206. synapse_sdk/utils/storage/providers/http.py +190 -0
  207. synapse_sdk/utils/storage/providers/s3.py +91 -0
  208. synapse_sdk/utils/storage/providers/sftp.py +47 -0
  209. synapse_sdk/utils/storage/registry.py +17 -0
  210. synapse_sdk-2025.12.3.dist-info/METADATA +123 -0
  211. synapse_sdk-2025.12.3.dist-info/RECORD +279 -0
  212. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/WHEEL +1 -1
  213. synapse_sdk/clients/backend/dataset.py +0 -51
  214. synapse_sdk/plugins/categories/import/actions/import.py +0 -10
  215. synapse_sdk/plugins/cli/__init__.py +0 -21
  216. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
  217. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
  218. synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
  219. synapse_sdk/utils/file.py +0 -168
  220. synapse_sdk/utils/storage.py +0 -91
  221. synapse_sdk-1.0.0a23.dist-info/METADATA +0 -44
  222. synapse_sdk-1.0.0a23.dist-info/RECORD +0 -114
  223. /synapse_sdk/{plugins/cli → cli/plugin}/run.py +0 -0
  224. /synapse_sdk/{plugins/categories/import → clients/validators}/__init__.py +0 -0
  225. /synapse_sdk/{plugins/categories/import/actions → devtools}/__init__.py +0 -0
  226. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/entry_points.txt +0 -0
  227. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info/licenses}/LICENSE +0 -0
  228. {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,687 @@
1
+ """Code-server integration for remote plugin development."""
2
+
3
+ import os
4
+ import shutil
5
+ import socket
6
+ import subprocess
7
+ import webbrowser
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ from urllib.parse import quote
11
+
12
+ import click
13
+ import inquirer
14
+ import yaml
15
+
16
+ from synapse_sdk.cli.config import fetch_agents_from_backend, get_agent_config
17
+ from synapse_sdk.devtools.config import get_backend_config
18
+ from synapse_sdk.utils.encryption import encrypt_plugin, get_plugin_info, is_plugin_directory
19
+
20
+
21
+ def get_agent_client(agent: Optional[str] = None):
22
+ """Helper function to get an agent client.
23
+
24
+ Args:
25
+ agent: Optional agent ID. If not provided, uses current agent or prompts user.
26
+
27
+ Returns:
28
+ tuple: (AgentClient instance, agent_id) or (None, None) if failed
29
+ """
30
+ # Get current agent configuration
31
+ agent_config = get_agent_config()
32
+ backend_config = get_backend_config()
33
+
34
+ if not backend_config:
35
+ click.echo("❌ No backend configured. Run 'synapse config' first.")
36
+ return None, None
37
+
38
+ # If no agent specified, use current agent or let user choose
39
+ if not agent:
40
+ if agent_config and agent_config.get('id'):
41
+ agent = agent_config['id']
42
+ click.echo(f'Using current agent: {agent_config.get("name", agent)}')
43
+ else:
44
+ # List available agents
45
+ agents, error = fetch_agents_from_backend()
46
+ if not agents:
47
+ click.echo('❌ No agents available. Check your backend configuration.')
48
+ return None, None
49
+
50
+ if len(agents) == 1:
51
+ # If only one agent, use it
52
+ agent = agents[0]['id']
53
+ click.echo(f'Using agent: {agents[0].get("name", agent)}')
54
+ else:
55
+ # Let user choose
56
+ click.echo('Available agents:')
57
+ for i, agent_info in enumerate(agents, 1):
58
+ status = agent_info.get('status_display', 'Unknown')
59
+ name = agent_info.get('name', agent_info['id'])
60
+ click.echo(f' {i}. {name} ({status})')
61
+
62
+ try:
63
+ choice = click.prompt('Select agent', type=int)
64
+ if 1 <= choice <= len(agents):
65
+ agent = agents[choice - 1]['id']
66
+ else:
67
+ click.echo('❌ Invalid selection')
68
+ return None, None
69
+ except (ValueError, EOFError, KeyboardInterrupt):
70
+ click.echo('\n❌ Cancelled')
71
+ return None, None
72
+
73
+ # Get agent details from backend
74
+ try:
75
+ from synapse_sdk.clients.backend import BackendClient
76
+
77
+ backend_client = BackendClient(backend_config['host'], access_token=backend_config['token'])
78
+
79
+ # Get agent information
80
+ try:
81
+ agent_info = backend_client._get(f'agents/{agent}/')
82
+ except Exception as e:
83
+ click.echo(f'❌ Failed to get agent information for: {agent}')
84
+ click.echo(f'Error: {e}')
85
+ return None, None
86
+
87
+ if not agent_info or not agent_info.get('url'):
88
+ click.echo(f'❌ Agent {agent} does not have a valid URL')
89
+ return None, None
90
+
91
+ # Get the agent token from local configuration
92
+ agent_token = agent_config.get('token')
93
+ if not agent_token:
94
+ click.echo('❌ No agent token found in configuration')
95
+ click.echo("Run 'synapse config' to configure the agent")
96
+ return None, None
97
+
98
+ # Create agent client
99
+ from synapse_sdk.clients.agent import AgentClient
100
+
101
+ client = AgentClient(base_url=agent_info['url'], agent_token=agent_token, user_token=backend_config['token'])
102
+ return client, agent
103
+
104
+ except Exception as e:
105
+ click.echo(f'❌ Failed to connect to agent: {e}')
106
+ return None, None
107
+
108
+
109
+ def detect_and_encrypt_plugin(workspace_path: str) -> Optional[dict]:
110
+ """Detect and encrypt plugin code in the workspace.
111
+
112
+ Args:
113
+ workspace_path: Path to check for plugin
114
+
115
+ Returns:
116
+ dict: Encrypted plugin data or None if no plugin found
117
+ """
118
+ plugin_path = Path(workspace_path)
119
+
120
+ if not is_plugin_directory(plugin_path):
121
+ return None
122
+
123
+ try:
124
+ plugin_info = get_plugin_info(plugin_path)
125
+ click.echo(f'🔍 Detected plugin: {plugin_info["name"]}')
126
+
127
+ if 'version' in plugin_info:
128
+ click.echo(f' Version: {plugin_info["version"]}')
129
+ if 'description' in plugin_info:
130
+ click.echo(f' Description: {plugin_info["description"]}')
131
+
132
+ click.echo('🔐 Encrypting plugin code...')
133
+ encrypted_package, password = encrypt_plugin(plugin_path)
134
+
135
+ # Add password to the package (in real implementation, this would be handled securely)
136
+ encrypted_package['password'] = password
137
+
138
+ click.echo('✅ Plugin code encrypted successfully')
139
+ return encrypted_package
140
+
141
+ except Exception as e:
142
+ click.echo(f'❌ Failed to encrypt plugin: {e}')
143
+ return None
144
+
145
+
146
+ def is_ssh_session() -> bool:
147
+ """Check if we're in an SSH session.
148
+
149
+ Returns:
150
+ bool: True if in SSH session, False otherwise
151
+ """
152
+ return bool(os.environ.get('SSH_CONNECTION') or os.environ.get('SSH_CLIENT'))
153
+
154
+
155
+ def is_vscode_terminal() -> bool:
156
+ """Check if we're running in a VSCode terminal.
157
+
158
+ Returns:
159
+ bool: True if in VSCode terminal, False otherwise
160
+ """
161
+ return os.environ.get('TERM_PROGRAM') == 'vscode'
162
+
163
+
164
+ def is_vscode_ssh_session() -> bool:
165
+ """Check if we're in a VSCode terminal over SSH.
166
+
167
+ Returns:
168
+ bool: True if in VSCode terminal via SSH, False otherwise
169
+ """
170
+ return is_vscode_terminal() and is_ssh_session()
171
+
172
+
173
+ def get_ssh_tunnel_instructions(port: int) -> str:
174
+ """Get SSH tunnel setup instructions for VSCode users.
175
+
176
+ Args:
177
+ port: The port number code-server is running on
178
+
179
+ Returns:
180
+ str: Instructions for setting up SSH tunnel
181
+ """
182
+ ssh_client = os.environ.get('SSH_CLIENT', '').split()[0] if os.environ.get('SSH_CLIENT') else 'your_server'
183
+
184
+ instructions = f"""
185
+ 📡 VSCode SSH Tunnel Setup:
186
+
187
+ Since you're using VSCode's integrated terminal over SSH, you can access code-server locally by:
188
+
189
+ 1. Using VSCode's built-in port forwarding:
190
+ • Open Command Palette (Cmd/Ctrl+Shift+P)
191
+ • Type "Forward a Port"
192
+ • Enter port: {port}
193
+ • VSCode will automatically forward the port
194
+
195
+ 2. Or manually forward the port in a new local terminal:
196
+ ssh -L {port}:localhost:{port} {ssh_client}
197
+
198
+ 3. Then open in your local browser:
199
+ http://localhost:{port}
200
+ """
201
+ return instructions
202
+
203
+
204
+ def open_browser_smart(url: str) -> bool:
205
+ """Open browser with smart fallback handling.
206
+
207
+ Attempts to open a browser using multiple methods, with appropriate
208
+ handling for SSH sessions and headless environments.
209
+
210
+ Args:
211
+ url: URL to open
212
+
213
+ Returns:
214
+ bool: True if browser was opened successfully, False otherwise
215
+ """
216
+ # Don't even try to open browser in SSH sessions (except VSCode can handle it)
217
+ if is_ssh_session() and not is_vscode_terminal():
218
+ return False
219
+
220
+ # Try Python's webbrowser module first (cross-platform)
221
+ try:
222
+ if webbrowser.open(url):
223
+ return True
224
+ except Exception:
225
+ pass
226
+
227
+ # Try platform-specific commands
228
+ commands = []
229
+
230
+ # Check for macOS
231
+ if shutil.which('open'):
232
+ commands.append(['open', url])
233
+
234
+ # Check for Linux with display
235
+ if os.environ.get('DISPLAY'):
236
+ if shutil.which('xdg-open'):
237
+ commands.append(['xdg-open', url])
238
+ if shutil.which('gnome-open'):
239
+ commands.append(['gnome-open', url])
240
+ if shutil.which('kde-open'):
241
+ commands.append(['kde-open', url])
242
+
243
+ # Try each command
244
+ for cmd in commands:
245
+ try:
246
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=2)
247
+ if result.returncode == 0:
248
+ return True
249
+ except Exception:
250
+ continue
251
+
252
+ return False
253
+
254
+
255
+ def is_port_in_use(port: int) -> bool:
256
+ """Check if a port is already in use.
257
+
258
+ Args:
259
+ port: Port number to check
260
+
261
+ Returns:
262
+ bool: True if port is in use, False otherwise
263
+ """
264
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
265
+ try:
266
+ s.bind(('127.0.0.1', port))
267
+ return False
268
+ except socket.error:
269
+ return True
270
+
271
+
272
+ def get_process_on_port(port: int) -> Optional[str]:
273
+ """Get the name of the process using a specific port.
274
+
275
+ Args:
276
+ port: Port number to check
277
+
278
+ Returns:
279
+ str: Process name if found, None otherwise
280
+ """
281
+ try:
282
+ # Use lsof to find the process
283
+ result = subprocess.run(['lsof', '-i', f':{port}', '-t'], capture_output=True, text=True, timeout=2)
284
+
285
+ if result.returncode == 0 and result.stdout.strip():
286
+ # Get the PID
287
+ pid = result.stdout.strip().split('\n')[0]
288
+
289
+ # Get process details
290
+ proc_result = subprocess.run(['ps', '-p', pid, '-o', 'comm='], capture_output=True, text=True, timeout=2)
291
+
292
+ if proc_result.returncode == 0:
293
+ process_name = proc_result.stdout.strip()
294
+ # Check if it's a code-server process
295
+ if 'node' in process_name or 'code-server' in process_name:
296
+ # Try to get more details
297
+ cmd_result = subprocess.run(
298
+ ['ps', '-p', pid, '-o', 'args='], capture_output=True, text=True, timeout=2
299
+ )
300
+ if cmd_result.returncode == 0 and 'code-server' in cmd_result.stdout:
301
+ return 'code-server'
302
+ return process_name
303
+ except Exception:
304
+ pass
305
+
306
+ return None
307
+
308
+
309
+ def is_code_server_installed() -> bool:
310
+ """Check if code-server is installed locally.
311
+
312
+ Returns:
313
+ bool: True if code-server is available in PATH
314
+ """
315
+ return shutil.which('code-server') is not None
316
+
317
+
318
+ def get_code_server_port() -> int:
319
+ """Get code-server port from config file.
320
+
321
+ Returns:
322
+ int: Port number from config, defaults to 8070 if not found
323
+ """
324
+ config_path = Path.home() / '.config' / 'code-server' / 'config.yaml'
325
+
326
+ try:
327
+ if config_path.exists():
328
+ with open(config_path, 'r') as f:
329
+ config = yaml.safe_load(f)
330
+
331
+ # Parse bind-addr which can be in format "127.0.0.1:8070" or just ":8070"
332
+ bind_addr = config.get('bind-addr', '')
333
+ if ':' in bind_addr:
334
+ port_str = bind_addr.split(':')[-1]
335
+ try:
336
+ return int(port_str)
337
+ except ValueError:
338
+ pass
339
+ except Exception:
340
+ # If any error occurs reading config, fall back to default
341
+ pass
342
+
343
+ # Default port if config not found or invalid
344
+ return 8070
345
+
346
+
347
+ def launch_local_code_server(workspace_path: str, open_browser: bool = True, custom_port: Optional[int] = None) -> None:
348
+ """Launch local code-server instance.
349
+
350
+ Args:
351
+ workspace_path: Directory to open in code-server
352
+ open_browser: Whether to open browser automatically
353
+ custom_port: Optional custom port to use instead of config/default
354
+ """
355
+ try:
356
+ # Use custom port if provided, otherwise get from config
357
+ port = custom_port if custom_port else get_code_server_port()
358
+
359
+ # Check if port is already in use
360
+ if is_port_in_use(port):
361
+ process_name = get_process_on_port(port)
362
+
363
+ if process_name == 'code-server':
364
+ # Code-server is already running
365
+ click.echo(f'⚠️ Code-server is already running on port {port}')
366
+
367
+ # Create URL with folder query parameter
368
+ encoded_path = quote(workspace_path)
369
+ url_with_folder = f'http://localhost:{port}/?folder={encoded_path}'
370
+
371
+ # Ask user what to do
372
+ questions = [
373
+ inquirer.List(
374
+ 'action',
375
+ message='What would you like to do?',
376
+ choices=[
377
+ ('Use existing code-server instance', 'use_existing'),
378
+ ('Stop existing and start new instance', 'restart'),
379
+ ('Cancel', 'cancel'),
380
+ ],
381
+ )
382
+ ]
383
+
384
+ try:
385
+ answers = inquirer.prompt(questions)
386
+
387
+ if not answers or answers['action'] == 'cancel':
388
+ click.echo('Cancelled')
389
+ return
390
+
391
+ if answers['action'] == 'use_existing':
392
+ click.echo('\n✅ Using existing code-server instance')
393
+ click.echo(f' URL: {url_with_folder}')
394
+
395
+ # Optionally open browser
396
+ if open_browser:
397
+ if is_vscode_ssh_session():
398
+ # Special handling for VSCode SSH sessions
399
+ click.echo(get_ssh_tunnel_instructions(port))
400
+ click.echo(f'🔗 Remote URL: {url_with_folder}')
401
+ click.echo(f'\n✨ After port forwarding, access at: http://localhost:{port}')
402
+ elif is_ssh_session():
403
+ click.echo('📝 SSH session detected - please open the URL in your local browser')
404
+ click.echo(f'👉 URL: {url_with_folder}')
405
+ elif open_browser_smart(url_with_folder):
406
+ click.echo('✅ Browser opened successfully')
407
+ else:
408
+ click.echo('⚠️ Could not open browser automatically')
409
+ click.echo(f'👉 URL: {url_with_folder}')
410
+ return
411
+
412
+ if answers['action'] == 'restart':
413
+ # Stop existing code-server
414
+ click.echo('Stopping existing code-server...')
415
+ try:
416
+ # Get PID of code-server process
417
+ result = subprocess.run(
418
+ ['lsof', '-i', f':{port}', '-t'], capture_output=True, text=True, timeout=2
419
+ )
420
+
421
+ if result.returncode == 0 and result.stdout.strip():
422
+ pid = result.stdout.strip().split('\n')[0]
423
+ subprocess.run(['kill', pid], timeout=5)
424
+
425
+ # Wait a moment for process to stop
426
+ import time
427
+
428
+ time.sleep(2)
429
+
430
+ click.echo('✅ Existing code-server stopped')
431
+ except Exception as e:
432
+ click.echo(f'⚠️ Could not stop existing code-server: {e}')
433
+ click.echo('Please stop it manually and try again')
434
+ return
435
+
436
+ except (KeyboardInterrupt, EOFError):
437
+ click.echo('\nCancelled')
438
+ return
439
+
440
+ else:
441
+ # Another process is using the port
442
+ click.echo(f'❌ Port {port} is already in use by: {process_name or "unknown process"}')
443
+ if not custom_port:
444
+ click.echo('\nYou can:')
445
+ click.echo('1. Stop the process using the port')
446
+ click.echo('2. Use a different port with --port option (e.g., --port 8071)')
447
+ click.echo('3. Change the default port in ~/.config/code-server/config.yaml')
448
+ else:
449
+ click.echo(f'Please try a different port or stop the process using port {port}')
450
+ return
451
+
452
+ # Create URL with folder query parameter
453
+ encoded_path = quote(workspace_path)
454
+ url_with_folder = f'http://localhost:{port}/?folder={encoded_path}'
455
+
456
+ # Basic code-server command - let code-server handle the workspace internally
457
+ cmd = ['code-server']
458
+
459
+ # Add custom port binding if specified
460
+ if custom_port:
461
+ cmd.extend(['--bind-addr', f'127.0.0.1:{port}'])
462
+
463
+ cmd.append(workspace_path)
464
+
465
+ if not open_browser:
466
+ cmd.append('--disable-getting-started-override')
467
+
468
+ click.echo(f'🚀 Starting local code-server for workspace: {workspace_path}')
469
+ if custom_port:
470
+ click.echo(f' Using custom port: {port}')
471
+ click.echo(f' URL: {url_with_folder}')
472
+ click.echo(' Press Ctrl+C to stop the server')
473
+
474
+ # Start code-server in background if we need to open browser
475
+ if open_browser:
476
+ # Start code-server in background
477
+ import threading
478
+ import time
479
+
480
+ def start_server():
481
+ subprocess.run(cmd)
482
+
483
+ server_thread = threading.Thread(target=start_server, daemon=True)
484
+ server_thread.start()
485
+
486
+ # Give server a moment to start
487
+ click.echo(' Waiting for server to start...')
488
+ time.sleep(3)
489
+
490
+ # Open browser with folder parameter
491
+ if is_vscode_ssh_session():
492
+ # Special handling for VSCode SSH sessions
493
+ click.echo(get_ssh_tunnel_instructions(port))
494
+ click.echo(f'🔗 Remote URL: {url_with_folder}')
495
+ click.echo(f'\n✨ After port forwarding, access at: http://localhost:{port}')
496
+ elif is_ssh_session():
497
+ click.echo('📝 SSH session detected - please open the URL in your local browser')
498
+ click.echo(f'👉 URL: {url_with_folder}')
499
+ elif open_browser_smart(url_with_folder):
500
+ click.echo('✅ Browser opened successfully')
501
+ else:
502
+ click.echo('⚠️ Could not open browser automatically')
503
+ click.echo(f'👉 Please manually open: {url_with_folder}')
504
+
505
+ # Wait for the server thread (blocking)
506
+ try:
507
+ server_thread.join()
508
+ except KeyboardInterrupt:
509
+ click.echo('\n\n✅ Code-server stopped')
510
+ else:
511
+ # Start code-server normally (blocking)
512
+ subprocess.run(cmd)
513
+
514
+ except KeyboardInterrupt:
515
+ click.echo('\n\n✅ Code-server stopped')
516
+ except Exception as e:
517
+ click.echo(f'❌ Failed to start local code-server: {e}')
518
+
519
+
520
+ def show_code_server_installation_help() -> None:
521
+ """Show installation instructions for code-server."""
522
+ click.echo('\n❌ Code-server is not installed locally')
523
+ click.echo('\n📦 To install code-server, choose one of these options:')
524
+ click.echo('\n1. Install script (recommended):')
525
+ click.echo(' curl -fsSL https://code-server.dev/install.sh | sh')
526
+ click.echo('\n2. Using npm:')
527
+ click.echo(' npm install -g code-server')
528
+ click.echo('\n3. Using yarn:')
529
+ click.echo(' yarn global add code-server')
530
+ click.echo('\n4. Download from releases:')
531
+ click.echo(' https://github.com/coder/code-server/releases')
532
+ click.echo('\n📚 For more installation options, visit: https://coder.com/docs/code-server/latest/install')
533
+
534
+
535
+ def run_agent_code_server(agent: Optional[str], workspace: str, open_browser: bool) -> None:
536
+ """Run code-server through agent (existing functionality).
537
+
538
+ Args:
539
+ agent: Agent name or ID
540
+ workspace: Workspace directory path
541
+ open_browser: Whether to open browser automatically
542
+ """
543
+ client, _ = get_agent_client(agent)
544
+ if not client:
545
+ return
546
+
547
+ # Check for plugin and show info if found
548
+ plugin_data = detect_and_encrypt_plugin(workspace)
549
+ if plugin_data:
550
+ click.echo('📦 Plugin detected and encrypted for secure transfer')
551
+
552
+ # Get code-server information
553
+ try:
554
+ info = client.get_code_server_info(workspace_path=workspace)
555
+ except Exception as e:
556
+ # Handle other errors
557
+ click.echo(f'❌ Failed to get code-server info: {e}')
558
+ click.echo('\nNote: The agent might not have code-server endpoint implemented yet.')
559
+ return
560
+
561
+ # Ensure info is a dictionary
562
+ if not isinstance(info, dict):
563
+ click.echo('❌ Invalid response from agent')
564
+ return
565
+
566
+ if not info.get('available', False):
567
+ message = info.get('message', 'Code-server is not available')
568
+ click.echo(f'❌ {message}')
569
+ click.echo('\nTo enable code-server, reinstall the agent with code-server support.')
570
+ return
571
+
572
+ # Display connection information
573
+ click.echo('\n✅ Code-Server is available!')
574
+
575
+ # Get the workspace path from response or use the requested one
576
+ actual_workspace = info.get('workspace', workspace)
577
+
578
+ # Show web browser access
579
+ click.echo('\n🌐 Web-based VS Code:')
580
+ url = info.get('url')
581
+ if not url:
582
+ click.echo('❌ No URL provided by agent')
583
+ return
584
+
585
+ click.echo(f' URL: {url}')
586
+ password = info.get('password')
587
+ if password:
588
+ click.echo(f' Password: {password}')
589
+ else:
590
+ click.echo(' Password: Not required (passwordless mode)')
591
+
592
+ # Show workspace information with better context
593
+ click.echo(f'\n📁 Agent Workspace: {actual_workspace}')
594
+ click.echo(f'📂 Local Project: {workspace}')
595
+
596
+ # Only show warning if the paths are drastically different and it's not the expected container path
597
+ if actual_workspace != workspace and not actual_workspace.startswith('/home/coder'):
598
+ click.echo(' ⚠️ Note: Agent workspace differs from local project path')
599
+
600
+ # Optionally open in browser
601
+ if open_browser and url:
602
+ click.echo('\nAttempting to open in browser...')
603
+
604
+ if is_vscode_ssh_session():
605
+ # Extract port from URL for instructions
606
+ import re
607
+
608
+ port_match = re.search(r':(\d+)', url)
609
+ if port_match:
610
+ agent_port = int(port_match.group(1))
611
+ click.echo(get_ssh_tunnel_instructions(agent_port))
612
+ click.echo(f'🔗 Remote URL: {url}')
613
+ elif is_ssh_session():
614
+ click.echo('📝 SSH session detected - please open the URL in your local browser')
615
+ click.echo(f'👉 URL: {url}')
616
+ elif open_browser_smart(url):
617
+ click.echo('✅ Browser opened successfully')
618
+ else:
619
+ click.echo('⚠️ Could not open browser automatically')
620
+ click.echo(f'👉 Please manually open: {url}')
621
+
622
+ # Show additional instructions
623
+ click.echo('\n📝 Quick Start:')
624
+ click.echo('1. Open the URL in your browser')
625
+ click.echo('2. Enter the password if prompted')
626
+ click.echo('3. Start coding in the web-based VS Code!')
627
+
628
+ # Add note about workspace synchronization
629
+ if actual_workspace.startswith('/home/coder'):
630
+ click.echo("\n💡 Note: Your local project files will be available in the agent's workspace.")
631
+ click.echo(' Changes made in code-server will be reflected in your local project.')
632
+
633
+
634
+ @click.command()
635
+ @click.option('--agent', help='Agent name or ID')
636
+ @click.option('--open-browser/--no-open-browser', default=True, help='Open in browser')
637
+ @click.option('--workspace', help='Workspace directory path (defaults to current directory)')
638
+ @click.option('--port', type=int, help='Port to bind code-server (default: from config or 8070)')
639
+ def code_server(agent: Optional[str], open_browser: bool, workspace: Optional[str], port: Optional[int]):
640
+ """Open code-server either through agent or locally."""
641
+
642
+ # Get current working directory if workspace not specified
643
+ if not workspace:
644
+ workspace = os.getcwd()
645
+
646
+ click.echo(f'Using workspace: {workspace}')
647
+
648
+ # Check if local code-server is available
649
+ local_available = is_code_server_installed()
650
+
651
+ # Create menu options based on availability
652
+ choices = []
653
+
654
+ # Always offer agent option
655
+ choices.append(('Open code-server through agent', 'agent'))
656
+
657
+ # Add local option if available
658
+ if local_available:
659
+ choices.append(('Open local code-server', 'local'))
660
+ else:
661
+ choices.append(('Install local code-server (not installed)', 'install'))
662
+
663
+ choices.append(('Cancel', 'cancel'))
664
+
665
+ # Show selection menu
666
+ questions = [inquirer.List('option', message='How would you like to open code-server?', choices=choices)]
667
+
668
+ try:
669
+ answers = inquirer.prompt(questions)
670
+ if not answers or answers['option'] == 'cancel':
671
+ click.echo('Cancelled')
672
+ return
673
+
674
+ if answers['option'] == 'agent':
675
+ click.echo('\n🤖 Opening code-server through agent...')
676
+ run_agent_code_server(agent, workspace, open_browser)
677
+
678
+ elif answers['option'] == 'local':
679
+ click.echo('\n💻 Starting local code-server...')
680
+ launch_local_code_server(workspace, open_browser, port)
681
+
682
+ elif answers['option'] == 'install':
683
+ show_code_server_installation_help()
684
+
685
+ except (KeyboardInterrupt, EOFError):
686
+ click.echo('\n\nCancelled')
687
+ return