powerbase-cli 0.1.0__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.
@@ -0,0 +1,5 @@
1
+ """Powerbase CLI package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
powerbase_cli/api.py ADDED
@@ -0,0 +1,348 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Iterable
7
+ from urllib.parse import urlencode
8
+
9
+ from .transport import PowerbaseTransport
10
+
11
+
12
+ def _response_data(response: dict[str, Any]) -> Any:
13
+ if response.get("success") is False:
14
+ raise RuntimeError(response.get("error") or response.get("message") or "Request failed")
15
+ return response.get("data", response)
16
+
17
+
18
+ class PowerbaseApi:
19
+ def __init__(self, transport: PowerbaseTransport) -> None:
20
+ self.transport = transport
21
+
22
+ def start_cli_login(self) -> dict[str, Any]:
23
+ return _response_data(
24
+ self.transport.invoke("cli-auth/start", method="POST", body={}, requires_auth=False)
25
+ )
26
+
27
+ def poll_cli_login(self, login_id: str) -> dict[str, Any]:
28
+ return _response_data(self.transport.invoke("cli-auth/poll", method="POST", body={"login_id": login_id}, requires_auth=False))
29
+
30
+ def list_orgs(self) -> Any:
31
+ return _response_data(self.transport.invoke("organizations", method="GET"))
32
+
33
+ def list_databases(self) -> Any:
34
+ return _response_data(self.transport.invoke("databases", method="GET"))
35
+
36
+ def get_database(self, database_id: str) -> Any:
37
+ return _response_data(self.transport.invoke(f"databases/{database_id}", method="GET"))
38
+
39
+ def create_database(
40
+ self,
41
+ name: str,
42
+ dsn: str,
43
+ description: str | None = None,
44
+ db_type: str | None = None,
45
+ ) -> Any:
46
+ body: dict[str, Any] = {"name": name, "dsn": dsn}
47
+ if description:
48
+ body["description"] = description
49
+ if db_type:
50
+ body["db_type"] = db_type
51
+ return _response_data(self.transport.invoke("databases", method="POST", body=body))
52
+
53
+ def update_database(
54
+ self,
55
+ database_id: str,
56
+ name: str | None = None,
57
+ dsn: str | None = None,
58
+ description: str | None = None,
59
+ db_type: str | None = None,
60
+ ) -> Any:
61
+ body: dict[str, Any] = {}
62
+ if name is not None:
63
+ body["name"] = name
64
+ if dsn is not None:
65
+ body["dsn"] = dsn
66
+ if description is not None:
67
+ body["description"] = description
68
+ if db_type is not None:
69
+ body["db_type"] = db_type
70
+ return _response_data(self.transport.invoke(f"databases/{database_id}", method="PUT", body=body))
71
+
72
+ def delete_database(self, database_id: str) -> Any:
73
+ return _response_data(self.transport.invoke(f"databases/{database_id}", method="DELETE"))
74
+
75
+ def list_instances(self) -> Any:
76
+ return _response_data(self.transport.invoke("instances", method="GET"))
77
+
78
+ def get_instance(self, instance_id: str) -> Any:
79
+ return _response_data(self.transport.invoke(f"instances/{instance_id}", method="GET"))
80
+
81
+ def create_instance(self, name: str | None, database_id: str | None, organization_id: str | None) -> Any:
82
+ body: dict[str, Any] = {}
83
+ if name:
84
+ body["name"] = name
85
+ if database_id:
86
+ body["database_id"] = database_id
87
+ if organization_id:
88
+ body["organization_id"] = organization_id
89
+ return _response_data(self.transport.invoke("instances", method="POST", body=body))
90
+
91
+ def delete_instance(self, instance_id: str) -> Any:
92
+ return _response_data(self.transport.invoke(f"instances/{instance_id}", method="DELETE"))
93
+
94
+ def list_branches(self, instance_id: str) -> Any:
95
+ return _response_data(self.transport.invoke(f"instances/{instance_id}/branches", method="GET"))
96
+
97
+ def create_branch(self, instance_id: str, branch_name: str, source_branch_slug: str | None) -> Any:
98
+ body = {
99
+ "branch_name": branch_name,
100
+ "source_branch_slug": source_branch_slug or "main",
101
+ }
102
+ return _response_data(self.transport.invoke(f"instances/{instance_id}/branches", method="POST", body=body))
103
+
104
+ def delete_branch(self, instance_id: str, branch_slug: str) -> Any:
105
+ return _response_data(self.transport.invoke(f"instances/{instance_id}/branches/{branch_slug}", method="DELETE"))
106
+
107
+ def switch_branch(self, instance_id: str, branch_name: str) -> Any:
108
+ return self.transport.invoke(
109
+ "sandbox-control/switch-branch",
110
+ method="POST",
111
+ body={"instance_id": instance_id, "branch_name": branch_name},
112
+ instance_id=instance_id,
113
+ )
114
+
115
+ def run_sql(self, instance_id: str, sql: str, branch: str | None) -> Any:
116
+ return self.transport.invoke(
117
+ "sql-execute",
118
+ method="POST",
119
+ body={"instance_id": instance_id, "sql": sql, "branch": branch or "main"},
120
+ instance_id=instance_id,
121
+ )
122
+
123
+ def publish_diff(self, instance_id: str, target: str = "prod") -> Any:
124
+ return self.transport.invoke(
125
+ f"sandbox-control/promote/diff?target={target}",
126
+ method="POST",
127
+ body={"instance_id": instance_id},
128
+ instance_id=instance_id,
129
+ )
130
+
131
+ def publish_run(self, instance_id: str, sql: str, target: str = "prod") -> Iterable[dict[str, Any]]:
132
+ resp = self.transport.invoke(
133
+ f"sandbox-control/promote/execute?target={target}&stream=1",
134
+ method="POST",
135
+ body={"instance_id": instance_id, "sql": sql},
136
+ headers={"Accept": "text/event-stream"},
137
+ instance_id=instance_id,
138
+ stream=True,
139
+ )
140
+ buffer = ""
141
+ while True:
142
+ chunk = resp.readline()
143
+ if not chunk:
144
+ break
145
+ line = chunk.decode("utf-8")
146
+ if line == "\n":
147
+ buffer = ""
148
+ continue
149
+ if line.startswith("data:"):
150
+ payload = line[5:].strip()
151
+ if payload:
152
+ yield json.loads(payload)
153
+
154
+ def sandbox_files_list(self, instance_id: str, path: str = "/", include_hidden: bool = False) -> Any:
155
+ params = {
156
+ "path": path,
157
+ "instance_id": instance_id,
158
+ }
159
+ if include_hidden:
160
+ params["includeHidden"] = "true"
161
+ suffix = urlencode(params)
162
+ return self.transport.invoke(f"sandbox-files/list?{suffix}", instance_id=instance_id)
163
+
164
+ def sandbox_files_tree(self, instance_id: str, root: str = "/", max_depth: int = 3) -> Any:
165
+ suffix = urlencode({"root": root, "maxDepth": max_depth, "instance_id": instance_id})
166
+ return self.transport.invoke(
167
+ f"sandbox-files/tree?{suffix}",
168
+ instance_id=instance_id,
169
+ )
170
+
171
+ def sandbox_files_read(self, instance_id: str, path: str) -> Any:
172
+ suffix = urlencode({"path": path, "instance_id": instance_id})
173
+ return self.transport.invoke(
174
+ f"sandbox-files/content?{suffix}",
175
+ instance_id=instance_id,
176
+ )
177
+
178
+ def sandbox_files_create_file(self, instance_id: str, path: str, content: str) -> Any:
179
+ return self.transport.invoke(
180
+ "sandbox-files/create",
181
+ method="POST",
182
+ body={"type": "file", "path": path, "content": content, "instance_id": instance_id},
183
+ instance_id=instance_id,
184
+ )
185
+
186
+ def sandbox_files_create_folder(self, instance_id: str, path: str) -> Any:
187
+ return self.transport.invoke(
188
+ "sandbox-files/create",
189
+ method="POST",
190
+ body={"type": "folder", "path": path, "instance_id": instance_id},
191
+ instance_id=instance_id,
192
+ )
193
+
194
+ def sandbox_files_upload(self, instance_id: str, source_path: str, target_path: str) -> Any:
195
+ file_bytes = Path(source_path).read_bytes()
196
+ encoded = base64.b64encode(file_bytes).decode("ascii")
197
+ return self.transport.invoke(
198
+ "sandbox-files/upload",
199
+ method="POST",
200
+ body={
201
+ "fileName": Path(source_path).name,
202
+ "fileContent": encoded,
203
+ "path": target_path,
204
+ "instance_id": instance_id,
205
+ },
206
+ instance_id=instance_id,
207
+ )
208
+
209
+ def sandbox_files_delete(self, instance_id: str, path: str) -> Any:
210
+ return self.transport.invoke(
211
+ "sandbox-files/delete",
212
+ method="POST",
213
+ body={"path": path, "instance_id": instance_id},
214
+ instance_id=instance_id,
215
+ )
216
+
217
+ def agent_providers(self, instance_id: str | None) -> Any:
218
+ return self.transport.invoke(
219
+ "sandbox-agent/providers",
220
+ instance_id=instance_id,
221
+ requires_auth=bool(instance_id),
222
+ )
223
+
224
+ def agent_status(self, instance_id: str, provider: str) -> Any:
225
+ return self.transport.invoke(
226
+ f"sandbox-agent/status?provider={provider}",
227
+ instance_id=instance_id,
228
+ )
229
+
230
+ def agent_login_url(self, instance_id: str, provider: str) -> Any:
231
+ return self.transport.invoke(
232
+ f"sandbox-agent/login-url?provider={provider}",
233
+ method="POST",
234
+ body={},
235
+ instance_id=instance_id,
236
+ )
237
+
238
+ def agent_opencode_config_get(self, instance_id: str) -> Any:
239
+ return self.transport.invoke(
240
+ "sandbox-agent/opencode-config",
241
+ method="GET",
242
+ instance_id=instance_id,
243
+ )
244
+
245
+ def agent_opencode_config_set(
246
+ self,
247
+ instance_id: str,
248
+ provider: str,
249
+ api_key: str,
250
+ models: list[dict[str, Any]] | None = None,
251
+ ) -> Any:
252
+ body: dict[str, Any] = {"provider": provider, "apiKey": api_key}
253
+ if models:
254
+ body["models"] = models
255
+ return self.transport.invoke(
256
+ "sandbox-agent/opencode-config",
257
+ method="POST",
258
+ body=body,
259
+ instance_id=instance_id,
260
+ )
261
+
262
+ def agent_opencode_config_delete(self, instance_id: str, provider: str) -> Any:
263
+ suffix = urlencode({"provider": provider})
264
+ return self.transport.invoke(
265
+ f"sandbox-agent/opencode-config?{suffix}",
266
+ method="DELETE",
267
+ instance_id=instance_id,
268
+ )
269
+
270
+ def agent_custom_provider_create(
271
+ self,
272
+ instance_id: str,
273
+ provider_id: str,
274
+ name: str | None,
275
+ base_url: str,
276
+ api_key: str,
277
+ models: list[dict[str, Any]],
278
+ ) -> Any:
279
+ body: dict[str, Any] = {
280
+ "id": provider_id,
281
+ "baseURL": base_url,
282
+ "apiKey": api_key,
283
+ "models": models,
284
+ }
285
+ if name:
286
+ body["name"] = name
287
+ return self.transport.invoke(
288
+ "sandbox-agent/opencode-custom-provider",
289
+ method="POST",
290
+ body=body,
291
+ instance_id=instance_id,
292
+ )
293
+
294
+ def agent_custom_provider_delete(self, instance_id: str, provider_id: str) -> Any:
295
+ suffix = urlencode({"id": provider_id})
296
+ return self.transport.invoke(
297
+ f"sandbox-agent/opencode-custom-provider?{suffix}",
298
+ method="DELETE",
299
+ instance_id=instance_id,
300
+ )
301
+
302
+ def agent_opencode_json_set(self, instance_id: str, payload: dict[str, Any]) -> Any:
303
+ return self.transport.invoke(
304
+ "sandbox-agent/opencode-json",
305
+ method="POST",
306
+ body=payload,
307
+ instance_id=instance_id,
308
+ )
309
+
310
+ def agent_chat(
311
+ self,
312
+ instance_id: str,
313
+ message: str,
314
+ *,
315
+ session_id: str | None = None,
316
+ provider: str | None = None,
317
+ model: str | None = None,
318
+ first_user_message: str | None = None,
319
+ agent: str | None = None,
320
+ ) -> Iterable[dict[str, Any]]:
321
+ body: dict[str, Any] = {"message": message}
322
+ if session_id:
323
+ body["session_id"] = session_id
324
+ if provider:
325
+ body["provider"] = provider
326
+ if model:
327
+ body["model"] = model
328
+ if first_user_message:
329
+ body["first_user_message"] = first_user_message
330
+ if agent:
331
+ body["agent"] = agent
332
+ resp = self.transport.invoke(
333
+ "sandbox-agent/chat",
334
+ method="POST",
335
+ body=body,
336
+ headers={"Accept": "text/event-stream"},
337
+ instance_id=instance_id,
338
+ stream=True,
339
+ )
340
+ while True:
341
+ chunk = resp.readline()
342
+ if not chunk:
343
+ break
344
+ line = chunk.decode("utf-8")
345
+ if line.startswith("data:"):
346
+ payload = line[5:].strip()
347
+ if payload:
348
+ yield json.loads(payload)
@@ -0,0 +1,21 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDhDCCAmygAwIBAgIUWu1JfeyC2qw56+7PUB5QY9w1MG8wDQYJKoZIhvcNAQEL
3
+ BQAwHjEcMBoGA1UEAwwTNi4xMi4yMzUuMTY1Lm5pcC5pbzAeFw0yNjA0MDcwNTUx
4
+ MjZaFw0yNzA0MDcwNTUxMjZaMB4xHDAaBgNVBAMMEzYuMTIuMjM1LjE2NS5uaXAu
5
+ aW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVaa9eQzG9t/WNcoP8
6
+ vJDX41oELoz/LOVzmr9zuFK3jQnIrVjFW8MdBHq1pOBLyHjKtnhKPo1n0br4/iPP
7
+ /TiJNwAuybN6iiSxWSsGpzFv7gIyJivdr65khzeLNda6HyBFxsRsVZAKKe8tueB4
8
+ FQM0YWxHkfAMw5bigDumZq3wzLUuvB7EtscVtl0dV0tcqovLq1Lg9/eBDSqfWuxv
9
+ hW9iROv23ryJETE/8Wh/2Gd5UV0E8CEX8As30+5nJ2jGF6O8abDRipvqc67+5v7h
10
+ dEXT2xIg4Fh8DAhlbgzM6Sj6I70pWiro+Q+N8GWvpJeJYGZu7J97eEOw+u1C2pCV
11
+ sbiBAgMBAAGjgbkwgbYwHQYDVR0OBBYEFODuAm1rmrl2GyZBcClH9HVE9kGMMB8G
12
+ A1UdIwQYMBaAFODuAm1rmrl2GyZBcClH9HVE9kGMMA8GA1UdEwEB/wQFMAMBAf8w
13
+ YwYDVR0RBFwwWoITNi4xMi4yMzUuMTY1Lm5pcC5pb4IbY29uc29sZS42LjEyLjIz
14
+ NS4xNjUubmlwLmlvghUqLjYuMTIuMjM1LjE2NS5uaXAuaW+CCWxvY2FsaG9zdIcE
15
+ fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAmvCeJ4Xnx8rXmU+oxiDiDZRfUK204Ta7
16
+ hztXu9LvxheW99p2R1AP2F7VUJOTq31HY9/r2p3qCEyz8F4nc/GPunHfF3y8mMiB
17
+ beFDdwrw8NWTRLGlOJynjsOXtkRkFa03DTKa4x9roZo10S+imDk1/DywWlv7UoAK
18
+ 9OsUB1zb8c6m7uZXrOTAZOzvO2fBrdymXnQ4tMvl54iyVT8X1rft5NUywfBzCtzB
19
+ g/OxLM2zJg3kz9kFztRuf1e07Tz6cqSy2CzClvhxaYJSfnlT2b73SceKy3kHn7Sh
20
+ VXA8yjqcTjk6wkMlHueIWnjnjYkRSpFjw3/ODNXbGJjxh+86l/ekPQ==
21
+ -----END CERTIFICATE-----
powerbase_cli/cli.py ADDED
@@ -0,0 +1,7 @@
1
+ from .commands.agent import handle_agent_chat
2
+ from .commands.auth import handle_auth_login
3
+ from .commands.parser import build_parser, main
4
+ from .commands.shared import build_api, resolve_config
5
+
6
+ __all__ = ["build_parser", "main", "build_api", "resolve_config", "handle_auth_login", "handle_agent_chat"]
7
+
@@ -0,0 +1,3 @@
1
+ from .parser import build_parser, main
2
+
3
+ __all__ = ["build_parser", "main"]
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+
6
+ from .shared import build_api, load_json_document, load_text, parse_json_specs, render_output, require_instance
7
+
8
+
9
+ def handle_agent_providers(args: argparse.Namespace) -> int:
10
+ _, _, context, _, api = build_api(args)
11
+ render_output(args, api.agent_providers(args.instance_id or context.instance_id))
12
+ return 0
13
+
14
+
15
+ def handle_agent_status(args: argparse.Namespace) -> int:
16
+ _, _, context, _, api = build_api(args)
17
+ render_output(args, api.agent_status(require_instance(args.instance_id, context), args.provider))
18
+ return 0
19
+
20
+
21
+ def handle_agent_login_url(args: argparse.Namespace) -> int:
22
+ _, _, context, _, api = build_api(args)
23
+ render_output(args, api.agent_login_url(require_instance(args.instance_id, context), args.provider))
24
+ return 0
25
+
26
+
27
+ def handle_agent_opencode_config_get(args: argparse.Namespace) -> int:
28
+ _, _, context, _, api = build_api(args)
29
+ render_output(args, api.agent_opencode_config_get(require_instance(args.instance_id, context)))
30
+ return 0
31
+
32
+
33
+ def handle_agent_opencode_config_set(args: argparse.Namespace) -> int:
34
+ _, _, context, _, api = build_api(args)
35
+ instance_id = require_instance(args.instance_id, context)
36
+ models = parse_json_specs(args.model_limit, label="--model-limit entry")
37
+ render_output(
38
+ args,
39
+ api.agent_opencode_config_set(instance_id, args.provider, args.api_key, models or None),
40
+ )
41
+ return 0
42
+
43
+
44
+ def handle_agent_opencode_config_delete(args: argparse.Namespace) -> int:
45
+ _, _, context, _, api = build_api(args)
46
+ render_output(
47
+ args,
48
+ api.agent_opencode_config_delete(require_instance(args.instance_id, context), args.provider),
49
+ )
50
+ return 0
51
+
52
+
53
+ def handle_agent_opencode_json_set(args: argparse.Namespace) -> int:
54
+ _, _, context, _, api = build_api(args)
55
+ payload = load_json_document(args.file)
56
+ render_output(
57
+ args,
58
+ api.agent_opencode_json_set(require_instance(args.instance_id, context), payload),
59
+ )
60
+ return 0
61
+
62
+
63
+ def handle_agent_chat(args: argparse.Namespace) -> int:
64
+ _, _, context, _, api = build_api(args)
65
+ instance_id = require_instance(args.instance_id, context)
66
+ message = load_text(
67
+ args.message,
68
+ args.message_file,
69
+ required_message="A chat message is required. Pass --message or --message-file.",
70
+ )
71
+ first_user_message = None
72
+ if args.first_user_message or args.first_user_message_file:
73
+ first_user_message = load_text(
74
+ args.first_user_message,
75
+ args.first_user_message_file,
76
+ required_message="first_user_message input is missing.",
77
+ )
78
+ events = api.agent_chat(
79
+ instance_id,
80
+ message,
81
+ session_id=args.session_id,
82
+ provider=args.provider,
83
+ model=args.model,
84
+ first_user_message=first_user_message,
85
+ agent=args.agent_name,
86
+ )
87
+ if args.stream_jsonl:
88
+ for event in events:
89
+ print(json.dumps(event, ensure_ascii=False))
90
+ return 0
91
+ render_output(args, {"events": list(events)})
92
+ return 0
93
+
94
+
95
+ def register_agent_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
96
+ agent = subparsers.add_parser("agent", help="Agent provider commands.", description="Inspect sandbox agent providers.")
97
+ agent_sub = agent.add_subparsers(dest="agent_command")
98
+ p = agent_sub.add_parser("providers", help="List providers.", description="List available sandbox agent providers.")
99
+ p.add_argument("--instance-id", help="Optional instance ID.")
100
+ p.set_defaults(handler=handle_agent_providers)
101
+ p = agent_sub.add_parser("status", help="Show provider status.", description="Show login status for one agent provider.")
102
+ p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
103
+ p.add_argument("--provider", default="cursor", help="Provider ID. Example: cursor")
104
+ p.set_defaults(handler=handle_agent_status)
105
+ p = agent_sub.add_parser(
106
+ "login-url",
107
+ help="Get a provider login URL.",
108
+ description="Get a browser login URL for one agent provider.",
109
+ )
110
+ p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
111
+ p.add_argument("--provider", default="cursor", help="Provider ID. Example: cursor")
112
+ p.set_defaults(handler=handle_agent_login_url)
113
+
114
+ p = agent_sub.add_parser(
115
+ "chat",
116
+ help="Send a prompt to the sandbox agent.",
117
+ description=(
118
+ "Send one prompt to the sandbox agent running in an instance. Use this when you want the sandbox "
119
+ "itself to inspect or modify the project. The automatic preview build follows the sandbox's current "
120
+ "branch, so run `powerbase branch switch` first when you want a feature-branch preview. By default "
121
+ "the command collects all streamed events and prints them as JSON. Use --stream-jsonl to print one "
122
+ "JSON event per line as events arrive."
123
+ ),
124
+ )
125
+ p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
126
+ p.add_argument("--provider", default="cursor", help="Agent provider ID, such as cursor, opencode, or kiro.")
127
+ p.add_argument("--session-id", help="Optional provider session ID to resume an existing conversation.")
128
+ p.add_argument("--model", help="Optional model ID for providers that support explicit model selection.")
129
+ p.add_argument("--agent", dest="agent_name", help="Optional Kiro agent name.")
130
+ p.add_argument("--message", help="Prompt text. Prefix with @ to load from a file.")
131
+ p.add_argument("--message-file", help="Path to a file containing the prompt text.")
132
+ p.add_argument(
133
+ "--first-user-message",
134
+ help="Optional first user message for language alignment during auto-fix. Prefix with @ to load from a file.",
135
+ )
136
+ p.add_argument("--first-user-message-file", help="Path to a file containing the first user message.")
137
+ p.add_argument("--stream-jsonl", action="store_true", help="Print one JSON event per line while the chat runs.")
138
+ p.set_defaults(handler=handle_agent_chat)
139
+
140
+ opencode_config = agent_sub.add_parser(
141
+ "opencode-config",
142
+ help="Manage opencode API-key config.",
143
+ description="Read or modify the sandbox opencode auth/config files for one instance.",
144
+ )
145
+ opencode_config_sub = opencode_config.add_subparsers(dest="agent_opencode_config_command")
146
+ p = opencode_config_sub.add_parser(
147
+ "get",
148
+ help="Show opencode config.",
149
+ description="Show configured providers, custom providers, and model limit overrides for one instance.",
150
+ )
151
+ p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
152
+ p.set_defaults(handler=handle_agent_opencode_config_get)
153
+ p = opencode_config_sub.add_parser(
154
+ "set",
155
+ help="Save one provider API key.",
156
+ description=(
157
+ "Save or update one opencode provider API key for an instance. Repeat --model-limit with JSON objects like "
158
+ '\'{"id":"claude-sonnet-4","limit":{"context":200000,"output":16000}}\'.'
159
+ ),
160
+ )
161
+ p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
162
+ p.add_argument("--provider", required=True, help="Provider ID to configure, such as anthropic or openai.")
163
+ p.add_argument("--api-key", required=True, help="API key to store in the sandbox.")
164
+ p.add_argument(
165
+ "--model-limit",
166
+ action="append",
167
+ help="Optional JSON object describing one model limit override.",
168
+ )
169
+ p.set_defaults(handler=handle_agent_opencode_config_set)
170
+ p = opencode_config_sub.add_parser(
171
+ "delete",
172
+ help="Delete one provider API key.",
173
+ description="Remove one opencode provider API key from the sandbox auth file.",
174
+ )
175
+ p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
176
+ p.add_argument("--provider", required=True, help="Provider ID to remove.")
177
+ p.set_defaults(handler=handle_agent_opencode_config_delete)
178
+
179
+ opencode_json = agent_sub.add_parser(
180
+ "opencode-json",
181
+ help="Replace the sandbox opencode.json file.",
182
+ description="Replace the sandbox opencode.json file with the contents of one local JSON document.",
183
+ )
184
+ opencode_json_sub = opencode_json.add_subparsers(dest="agent_opencode_json_command")
185
+ p = opencode_json_sub.add_parser(
186
+ "set",
187
+ help="Replace opencode.json.",
188
+ description="Load one local JSON file and replace the sandbox opencode.json file with it.",
189
+ )
190
+ p.add_argument("--instance-id", help="Instance ID. Falls back to context.json.")
191
+ p.add_argument("--file", required=True, help="Path to a local JSON file.")
192
+ p.set_defaults(handler=handle_agent_opencode_json_set)