codex-python 1.131.0__tar.gz → 1.140.0__tar.gz

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 (42) hide show
  1. {codex_python-1.131.0 → codex_python-1.140.0}/PKG-INFO +1 -1
  2. {codex_python-1.131.0 → codex_python-1.140.0}/codex/__init__.py +1 -1
  3. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_async_client.py +33 -2
  4. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_async_services.py +74 -0
  5. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_async_threads.py +41 -5
  6. codex_python-1.140.0/codex/app_server/_payloads.py +102 -0
  7. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_sync_client.py +2 -0
  8. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_sync_services.py +83 -0
  9. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_sync_threads.py +42 -5
  10. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/options.py +66 -15
  11. {codex_python-1.131.0 → codex_python-1.140.0}/codex/options.py +52 -4
  12. {codex_python-1.131.0 → codex_python-1.140.0}/codex/protocol/types.py +1505 -725
  13. {codex_python-1.131.0 → codex_python-1.140.0}/crates/codex_native/Cargo.lock +9 -9
  14. {codex_python-1.131.0 → codex_python-1.140.0}/crates/codex_native/Cargo.toml +1 -1
  15. codex_python-1.131.0/codex/app_server/_payloads.py +0 -59
  16. {codex_python-1.131.0 → codex_python-1.140.0}/LICENSE +0 -0
  17. {codex_python-1.131.0 → codex_python-1.140.0}/README.md +0 -0
  18. {codex_python-1.131.0 → codex_python-1.140.0}/codex/_binary.py +0 -0
  19. {codex_python-1.131.0 → codex_python-1.140.0}/codex/_config_types.py +0 -0
  20. {codex_python-1.131.0 → codex_python-1.140.0}/codex/_file_utils.py +0 -0
  21. {codex_python-1.131.0 → codex_python-1.140.0}/codex/_runtime.py +0 -0
  22. {codex_python-1.131.0 → codex_python-1.140.0}/codex/_turn_options.py +0 -0
  23. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/__init__.py +0 -0
  24. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_protocol_helpers.py +0 -0
  25. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_session.py +0 -0
  26. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_sync_support.py +0 -0
  27. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/_types.py +0 -0
  28. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/errors.py +0 -0
  29. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/models.py +0 -0
  30. {codex_python-1.131.0 → codex_python-1.140.0}/codex/app_server/transports.py +0 -0
  31. {codex_python-1.131.0 → codex_python-1.140.0}/codex/codex.py +0 -0
  32. {codex_python-1.131.0 → codex_python-1.140.0}/codex/dynamic_tools.py +0 -0
  33. {codex_python-1.131.0 → codex_python-1.140.0}/codex/errors.py +0 -0
  34. {codex_python-1.131.0 → codex_python-1.140.0}/codex/output_schema.py +0 -0
  35. {codex_python-1.131.0 → codex_python-1.140.0}/codex/output_schema_file.py +0 -0
  36. {codex_python-1.131.0 → codex_python-1.140.0}/codex/protocol/__init__.py +0 -0
  37. {codex_python-1.131.0 → codex_python-1.140.0}/codex/py.typed +0 -0
  38. {codex_python-1.131.0 → codex_python-1.140.0}/codex/thread.py +0 -0
  39. {codex_python-1.131.0 → codex_python-1.140.0}/codex/vendor/.gitkeep +0 -0
  40. {codex_python-1.131.0 → codex_python-1.140.0}/crates/codex_native/codex/__init__.py +0 -0
  41. {codex_python-1.131.0 → codex_python-1.140.0}/crates/codex_native/src/lib.rs +0 -0
  42. {codex_python-1.131.0 → codex_python-1.140.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-python
3
- Version: 1.131.0
3
+ Version: 1.140.0
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3 :: Only
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -17,7 +17,7 @@ from codex.options import (
17
17
  )
18
18
  from codex.thread import CodexTurnStream, Input, Thread
19
19
 
20
- __version__ = "1.131.0"
20
+ __version__ = "1.140.0"
21
21
 
22
22
  __all__ = [
23
23
  "Codex",
@@ -14,6 +14,7 @@ from codex.app_server._async_services import (
14
14
  AsyncConfigClient,
15
15
  AsyncExternalAgentConfigClient,
16
16
  AsyncFeedbackClient,
17
+ AsyncFsClient,
17
18
  AsyncMcpServersClient,
18
19
  AsyncModelsClient,
19
20
  AsyncSkillsClient,
@@ -22,7 +23,7 @@ from codex.app_server._async_services import (
22
23
  from codex.app_server._async_threads import AsyncAppServerThread as AsyncAppServerThread
23
24
  from codex.app_server._async_threads import AsyncTurnStream as AsyncTurnStream
24
25
  from codex.app_server._async_threads import _ThreadClient
25
- from codex.app_server._protocol_helpers import RequestHandler
26
+ from codex.app_server._protocol_helpers import Notification, RequestHandler
26
27
  from codex.app_server._session import _AsyncNotificationSubscription, _AsyncSession
27
28
  from codex.app_server.models import (
28
29
  InitializeResult,
@@ -107,6 +108,35 @@ class AsyncEventsClient:
107
108
  def subscribe(self, methods: Collection[str] | None = None) -> _AsyncNotificationSubscription:
108
109
  return self._session.subscribe_notifications(methods)
109
110
 
111
+ def subscribe_command_exec_output(self, process_id: str) -> _AsyncNotificationSubscription:
112
+ """Subscribe to `command/exec/outputDelta` notifications for one process id."""
113
+
114
+ def predicate(notification: Notification) -> bool:
115
+ return (
116
+ isinstance(notification, protocol.CommandExecOutputDeltaNotificationModel)
117
+ and notification.params.processId == process_id
118
+ )
119
+
120
+ return self._session.subscribe_notifications(
121
+ {"command/exec/outputDelta"},
122
+ predicate=predicate,
123
+ )
124
+
125
+ def subscribe_process_events(self, process_handle: str) -> _AsyncNotificationSubscription:
126
+ """Subscribe to `process/outputDelta` and `process/exited` for one process handle."""
127
+
128
+ def predicate(notification: Notification) -> bool:
129
+ if isinstance(notification, protocol.ProcessOutputDeltaNotificationModel):
130
+ return notification.params.processHandle == process_handle
131
+ if isinstance(notification, protocol.ProcessExitedNotificationModel):
132
+ return notification.params.processHandle == process_handle
133
+ return False
134
+
135
+ return self._session.subscribe_notifications(
136
+ {"process/outputDelta", "process/exited"},
137
+ predicate=predicate,
138
+ )
139
+
110
140
 
111
141
  class AsyncAppServerClient:
112
142
  """Async client for `codex app-server`."""
@@ -122,7 +152,8 @@ class AsyncAppServerClient:
122
152
  self.events = AsyncEventsClient(self._session)
123
153
  self.models = AsyncModelsClient(self.rpc)
124
154
  self.apps = AsyncAppsClient(self.rpc)
125
- self.skills = AsyncSkillsClient(self.rpc)
155
+ self.fs = AsyncFsClient(self.rpc)
156
+ self.skills = AsyncSkillsClient(self.rpc, self.fs)
126
157
  self.account = AsyncAccountClient(self.rpc)
127
158
  self.config = AsyncConfigClient(self.rpc)
128
159
  self.mcp_servers = AsyncMcpServersClient(self.rpc)
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  from collections.abc import Mapping, Sequence
4
5
  from typing import Any, Protocol, TypeVar
5
6
 
6
7
  from pydantic import BaseModel
7
8
 
9
+ from codex.app_server._payloads import skill_input
8
10
  from codex.app_server.models import (
9
11
  AccountCancelLoginResult,
10
12
  AccountRateLimitsResult,
@@ -49,6 +51,16 @@ class _AsyncServiceClient:
49
51
  self._rpc = rpc
50
52
 
51
53
 
54
+ def _skill_markdown_path(directory: str) -> str:
55
+ if directory == "":
56
+ raise ValueError("Skill directory cannot be empty.")
57
+ if directory.endswith("/") or directory.endswith("\\"):
58
+ return f"{directory}SKILL.md"
59
+ if "\\" in directory and "/" not in directory:
60
+ return f"{directory}\\SKILL.md"
61
+ return f"{directory}/SKILL.md"
62
+
63
+
52
64
  class AsyncModelsClient(_AsyncServiceClient):
53
65
  async def list(
54
66
  self,
@@ -107,7 +119,50 @@ class AsyncAppsClient(_AsyncServiceClient):
107
119
  return await self._rpc.request_typed("app/list", params, AppListResult)
108
120
 
109
121
 
122
+ class AsyncFsClient(_AsyncServiceClient):
123
+ async def create_directory(
124
+ self,
125
+ *,
126
+ path: str,
127
+ recursive: bool | None = True,
128
+ ) -> protocol.FsCreateDirectoryResponse:
129
+ params = protocol.FsCreateDirectoryParams(
130
+ path=protocol.AbsolutePathBuf(path),
131
+ recursive=recursive,
132
+ )
133
+ return await self._rpc.request_typed(
134
+ "fs/createDirectory",
135
+ params,
136
+ protocol.FsCreateDirectoryResponse,
137
+ )
138
+
139
+ async def write_file(
140
+ self,
141
+ *,
142
+ path: str,
143
+ data: str | bytes,
144
+ encoding: str = "utf-8",
145
+ ) -> protocol.FsWriteFileResponse:
146
+ raw_data = data.encode(encoding) if isinstance(data, str) else data
147
+ params = protocol.FsWriteFileParams(
148
+ path=protocol.AbsolutePathBuf(path),
149
+ dataBase64=base64.b64encode(raw_data).decode("ascii"),
150
+ )
151
+ return await self._rpc.request_typed(
152
+ "fs/writeFile",
153
+ params,
154
+ protocol.FsWriteFileResponse,
155
+ )
156
+
157
+
110
158
  class AsyncSkillsClient(_AsyncServiceClient):
159
+ def __init__(self, rpc: _TypedRpcClient, fs: AsyncFsClient) -> None:
160
+ super().__init__(rpc)
161
+ self._fs = fs
162
+
163
+ def input(self, *, name: str, path: str) -> protocol.SkillUserInput:
164
+ return skill_input(name=name, path=path)
165
+
111
166
  async def list(
112
167
  self,
113
168
  *,
@@ -133,6 +188,9 @@ class AsyncSkillsClient(_AsyncServiceClient):
133
188
  )
134
189
  return await self._rpc.request_typed("skills/list", params, SkillsListResult)
135
190
 
191
+ async def reload(self, *, cwds: Sequence[str] | None = None) -> Sequence[SkillsListEntry]:
192
+ return await self.list(cwds=cwds, force_reload=True)
193
+
136
194
  async def write_config(self, *, path: str, enabled: bool) -> SkillsConfigWriteResult:
137
195
  params = protocol.SkillsConfigWriteParams(
138
196
  path=protocol.AbsolutePathBuf(path),
@@ -140,6 +198,20 @@ class AsyncSkillsClient(_AsyncServiceClient):
140
198
  )
141
199
  return await self._rpc.request_typed("skills/config/write", params, SkillsConfigWriteResult)
142
200
 
201
+ async def write_skill(
202
+ self,
203
+ *,
204
+ name: str,
205
+ directory: str,
206
+ instructions: str | bytes,
207
+ reload_cwds: Sequence[str] | None = None,
208
+ ) -> protocol.SkillUserInput:
209
+ skill_path = _skill_markdown_path(directory)
210
+ await self._fs.create_directory(path=directory, recursive=True)
211
+ await self._fs.write_file(path=skill_path, data=instructions)
212
+ await self.reload(cwds=reload_cwds)
213
+ return self.input(name=name, path=skill_path)
214
+
143
215
 
144
216
  class AsyncAccountClient(_AsyncServiceClient):
145
217
  async def read(self, *, refresh_token: bool | None = None) -> AccountReadResult:
@@ -324,6 +396,7 @@ class AsyncCommandClient(_AsyncServiceClient):
324
396
  disable_timeout: bool | None = None,
325
397
  env: Mapping[str, object | None] | None = None,
326
398
  output_bytes_cap: int | None = None,
399
+ permission_profile: str | None = None,
327
400
  process_id: str | None = None,
328
401
  sandbox_policy: protocol.SandboxPolicy | None = None,
329
402
  size: protocol.CommandExecTerminalSize | None = None,
@@ -339,6 +412,7 @@ class AsyncCommandClient(_AsyncServiceClient):
339
412
  disableTimeout=disable_timeout,
340
413
  env=dict(env) if env is not None else None,
341
414
  outputBytesCap=output_bytes_cap,
415
+ permissionProfile=permission_profile,
342
416
  processId=process_id,
343
417
  sandboxPolicy=sandbox_policy,
344
418
  size=size,
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- from collections.abc import Callable, Collection, Mapping
6
+ from collections.abc import Callable, Collection, Mapping, Sequence
7
7
  from dataclasses import dataclass
8
8
  from typing import Protocol, TypeVar, cast
9
9
 
@@ -37,9 +37,11 @@ _ModelT = TypeVar("_ModelT", bound=BaseModel)
37
37
  DEFAULT_REVIEW_DELIVERY = protocol.ReviewDelivery("inline")
38
38
 
39
39
  _TURN_STREAM_NOTIFICATION_METHODS = {
40
+ "error",
40
41
  "turn/started",
41
42
  "turn/completed",
42
43
  "turn/diff/updated",
44
+ "turn/moderationMetadata",
43
45
  "turn/plan/updated",
44
46
  "hook/started",
45
47
  "hook/completed",
@@ -54,6 +56,7 @@ _TURN_STREAM_NOTIFICATION_METHODS = {
54
56
  "item/reasoning/summaryPartAdded",
55
57
  "item/reasoning/textDelta",
56
58
  "item/commandExecution/outputDelta",
59
+ "item/commandExecution/terminalInteraction",
57
60
  "item/fileChange/outputDelta",
58
61
  "serverRequest/resolved",
59
62
  }
@@ -132,6 +135,7 @@ class AsyncTurnStream:
132
135
  self.usage: protocol.ThreadTokenUsage | None = None
133
136
  self._item_index: dict[str, int] = {}
134
137
  self._text_deltas: list[str] = []
138
+ self._retryable_error_notifications: list[protocol.ErrorNotificationModel] = []
135
139
  self._done = False
136
140
  self._closed = False
137
141
 
@@ -188,6 +192,16 @@ class AsyncTurnStream:
188
192
  raise StopAsyncIteration
189
193
  notification = await self._subscription.next()
190
194
  self._apply(notification)
195
+ if isinstance(notification, protocol.ErrorNotificationModel):
196
+ if not notification.params.willRetry:
197
+ self._done = True
198
+ await self.close()
199
+ error = notification.params.error
200
+ message = error.message
201
+ if error.additionalDetails is not None and error.additionalDetails != "":
202
+ message = f"{message}: {error.additionalDetails}"
203
+ raise AppServerTurnError(message)
204
+ self._retryable_error_notifications.append(notification)
191
205
  if isinstance(notification, protocol.TurnCompletedNotificationModel):
192
206
  self._done = True
193
207
  return notification
@@ -244,12 +258,13 @@ class AsyncTurnStream:
244
258
  input: TurnInput,
245
259
  *,
246
260
  responsesapi_client_metadata: Mapping[str, object] | None = None,
261
+ skills: Sequence[protocol.SkillUserInput] | None = None,
247
262
  ) -> TurnIdResult:
248
263
  """Append additional user input to the in-flight turn."""
249
264
  payload: dict[str, object] = {
250
265
  "threadId": self.thread_id,
251
266
  "expectedTurnId": self.turn_id,
252
- "input": normalize_turn_input(input),
267
+ "input": normalize_turn_input(input, skills=skills),
253
268
  }
254
269
  if responsesapi_client_metadata is not None:
255
270
  payload["responsesapiClientMetadata"] = dict(responsesapi_client_metadata)
@@ -274,6 +289,18 @@ class AsyncTurnStream:
274
289
  """Return the streamed agent text deltas received so far."""
275
290
  return tuple(self._text_deltas)
276
291
 
292
+ @property
293
+ def retryable_error_notifications(self) -> tuple[protocol.ErrorNotificationModel, ...]:
294
+ """Return retryable turn error notifications received so far."""
295
+ return tuple(self._retryable_error_notifications)
296
+
297
+ @property
298
+ def retryable_errors(self) -> tuple[protocol.TurnError, ...]:
299
+ """Return retryable turn errors received so far."""
300
+ return tuple(
301
+ notification.params.error for notification in self._retryable_error_notifications
302
+ )
303
+
277
304
  def _apply(self, notification: Notification) -> None:
278
305
  self._apply_text_delta(notification)
279
306
  self._apply_token_usage(notification)
@@ -391,11 +418,13 @@ class AsyncAppServerThread:
391
418
  self,
392
419
  input: TurnInput,
393
420
  options: AppServerTurnOptions | None = None,
421
+ *,
422
+ skills: Sequence[protocol.SkillUserInput] | None = None,
394
423
  ) -> AsyncTurnStream:
395
424
  """Start a turn and return the protocol-native notification stream."""
396
425
  payload = (options or AppServerTurnOptions()).to_params(
397
426
  thread_id=self.id,
398
- input=normalize_turn_input(input),
427
+ input=normalize_turn_input(input, skills=skills),
399
428
  )
400
429
  return await AsyncTurnStream.start(self, payload)
401
430
 
@@ -403,8 +432,10 @@ class AsyncAppServerThread:
403
432
  self,
404
433
  input: TurnInput,
405
434
  options: AppServerTurnOptions | None = None,
435
+ *,
436
+ skills: Sequence[protocol.SkillUserInput] | None = None,
406
437
  ) -> str:
407
- stream = await self.run(input, options)
438
+ stream = await self.run(input, options, skills=skills)
408
439
  await stream.wait()
409
440
  stream.raise_for_terminal_status()
410
441
  return stream.final_text
@@ -413,8 +444,10 @@ class AsyncAppServerThread:
413
444
  self,
414
445
  input: TurnInput,
415
446
  options: AppServerTurnOptions | None = None,
447
+ *,
448
+ skills: Sequence[protocol.SkillUserInput] | None = None,
416
449
  ) -> object:
417
- stream = await self.run(input, options)
450
+ stream = await self.run(input, options, skills=skills)
418
451
  await stream.wait()
419
452
  stream.raise_for_terminal_status()
420
453
  return stream.final_json()
@@ -424,6 +457,8 @@ class AsyncAppServerThread:
424
457
  input: TurnInput,
425
458
  model_type: type[_ModelT],
426
459
  options: AppServerTurnOptions | None = None,
460
+ *,
461
+ skills: Sequence[protocol.SkillUserInput] | None = None,
427
462
  ) -> _ModelT:
428
463
  """Run a turn and validate the final assistant text with `model_type`."""
429
464
  stream = await self.run(
@@ -433,6 +468,7 @@ class AsyncAppServerThread:
433
468
  model_type,
434
469
  owner="AppServerThread.run_model()",
435
470
  ),
471
+ skills=skills,
436
472
  )
437
473
  await stream.wait()
438
474
  stream.raise_for_terminal_status()
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any, cast
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from codex.app_server._types import JsonObject
9
+ from codex.protocol import types as protocol
10
+
11
+ InputItem = (
12
+ str
13
+ | Mapping[str, Any]
14
+ | protocol.UserInput
15
+ | protocol.TextUserInput
16
+ | protocol.ImageUserInput
17
+ | protocol.LocalImageUserInput
18
+ | protocol.SkillUserInput
19
+ | protocol.MentionUserInput
20
+ )
21
+ TurnInput = InputItem | Sequence[InputItem]
22
+
23
+ type ParamsModel = BaseModel
24
+
25
+
26
+ def serialize_value(value: object) -> object:
27
+ if isinstance(value, BaseModel):
28
+ return value.model_dump(
29
+ mode="json",
30
+ by_alias=True,
31
+ exclude_none=True,
32
+ exclude_unset=True,
33
+ )
34
+ if isinstance(value, type) and issubclass(value, BaseModel):
35
+ return value.model_json_schema()
36
+ if isinstance(value, list):
37
+ return [serialize_value(item) for item in value]
38
+ if isinstance(value, tuple):
39
+ return [serialize_value(item) for item in value]
40
+ if isinstance(value, Mapping):
41
+ return {key: serialize_value(item) for key, item in value.items()}
42
+ return value
43
+
44
+
45
+ def normalize_input_item(item: InputItem) -> JsonObject:
46
+ if isinstance(item, str):
47
+ return {"type": "text", "text": item}
48
+ serialized = serialize_value(item)
49
+ if not isinstance(serialized, dict):
50
+ raise TypeError(f"Input item must serialize to an object, got {type(serialized).__name__}")
51
+ return cast(JsonObject, serialized)
52
+
53
+
54
+ def skill_input(*, name: str, path: str) -> protocol.SkillUserInput:
55
+ if name == "":
56
+ raise ValueError("Skill name cannot be empty.")
57
+ if name.startswith("$"):
58
+ raise ValueError("Skill name should not include the '$' activation marker.")
59
+ if any(character.isspace() for character in name):
60
+ raise ValueError("Skill name cannot contain whitespace.")
61
+ if "/" in name or "\\" in name:
62
+ raise ValueError("Skill name cannot contain path separators.")
63
+ if path == "":
64
+ raise ValueError("Skill path cannot be empty.")
65
+ return protocol.SkillUserInput(
66
+ type=protocol.SkillUserInputType("skill"),
67
+ name=name,
68
+ path=path,
69
+ )
70
+
71
+
72
+ def _add_skill_markers(
73
+ items: list[JsonObject],
74
+ skills: Sequence[protocol.SkillUserInput],
75
+ ) -> list[JsonObject]:
76
+ if not skills:
77
+ return items
78
+ markers = " ".join(f"${skill.name}" for skill in skills)
79
+ for item in items:
80
+ if item.get("type") != "text":
81
+ continue
82
+ text = item.get("text")
83
+ if isinstance(text, str):
84
+ item["text"] = f"{markers}\n\n{text}"
85
+ return items
86
+ return [{"type": "text", "text": markers}, *items]
87
+
88
+
89
+ def normalize_turn_input(
90
+ value: TurnInput,
91
+ *,
92
+ skills: Sequence[protocol.SkillUserInput] | None = None,
93
+ ) -> list[JsonObject]:
94
+ if isinstance(value, str):
95
+ items = [{"type": "text", "text": value}]
96
+ elif isinstance(value, Sequence):
97
+ items = [normalize_input_item(item) for item in value]
98
+ else:
99
+ items = [normalize_input_item(value)]
100
+ if skills is None:
101
+ return items
102
+ return _add_skill_markers(items, skills) + [normalize_input_item(skill) for skill in skills]
@@ -21,6 +21,7 @@ from codex.app_server._sync_services import (
21
21
  _ConfigClient,
22
22
  _ExternalAgentConfigClient,
23
23
  _FeedbackClient,
24
+ _FsClient,
24
25
  _McpServersClient,
25
26
  _ModelsClient,
26
27
  _SkillsClient,
@@ -240,6 +241,7 @@ class AppServerClient(_SyncRunner):
240
241
  self.mcp_servers = _McpServersClient(async_client.mcp_servers, self._run)
241
242
  self.feedback = _FeedbackClient(async_client.feedback, self._run)
242
243
  self.command = _CommandClient(async_client.command, self._run)
244
+ self.fs = _FsClient(async_client.fs, self._run)
243
245
  self.external_agent_config = _ExternalAgentConfigClient(
244
246
  async_client.external_agent_config,
245
247
  self._run,
@@ -71,6 +71,8 @@ class _AsyncAppsClientLike(Protocol):
71
71
 
72
72
 
73
73
  class _AsyncSkillsClientLike(Protocol):
74
+ def input(self, *, name: str, path: str) -> protocol.SkillUserInput: ...
75
+
74
76
  async def list(
75
77
  self,
76
78
  *,
@@ -85,8 +87,36 @@ class _AsyncSkillsClientLike(Protocol):
85
87
  force_reload: bool | None = None,
86
88
  ) -> SkillsListResult: ...
87
89
 
90
+ async def reload(self, *, cwds: Sequence[str] | None = None) -> Sequence[SkillsListEntry]: ...
91
+
88
92
  async def write_config(self, *, path: str, enabled: bool) -> SkillsConfigWriteResult: ...
89
93
 
94
+ async def write_skill(
95
+ self,
96
+ *,
97
+ name: str,
98
+ directory: str,
99
+ instructions: str | bytes,
100
+ reload_cwds: Sequence[str] | None = None,
101
+ ) -> protocol.SkillUserInput: ...
102
+
103
+
104
+ class _AsyncFsClientLike(Protocol):
105
+ async def create_directory(
106
+ self,
107
+ *,
108
+ path: str,
109
+ recursive: bool | None = True,
110
+ ) -> protocol.FsCreateDirectoryResponse: ...
111
+
112
+ async def write_file(
113
+ self,
114
+ *,
115
+ path: str,
116
+ data: str | bytes,
117
+ encoding: str = "utf-8",
118
+ ) -> protocol.FsWriteFileResponse: ...
119
+
90
120
 
91
121
  class _AsyncAccountClientLike(Protocol):
92
122
  async def read(self, *, refresh_token: bool | None = None) -> AccountReadResult: ...
@@ -187,6 +217,7 @@ class _AsyncCommandClientLike(Protocol):
187
217
  disable_timeout: bool | None = None,
188
218
  env: Mapping[str, object | None] | None = None,
189
219
  output_bytes_cap: int | None = None,
220
+ permission_profile: str | None = None,
190
221
  process_id: str | None = None,
191
222
  sandbox_policy: protocol.SandboxPolicy | None = None,
192
223
  size: protocol.CommandExecTerminalSize | None = None,
@@ -331,6 +362,9 @@ class _SkillsClient(_SyncRunner):
331
362
  super().__init__(runner)
332
363
  self._async_client = async_client
333
364
 
365
+ def input(self, *, name: str, path: str) -> protocol.SkillUserInput:
366
+ return self._async_client.input(name=name, path=path)
367
+
334
368
  def list(
335
369
  self,
336
370
  *,
@@ -357,9 +391,56 @@ class _SkillsClient(_SyncRunner):
357
391
  )
358
392
  )
359
393
 
394
+ def reload(self, *, cwds: Sequence[str] | None = None) -> Sequence[SkillsListEntry]:
395
+ return self._run(self._async_client.reload(cwds=cwds))
396
+
360
397
  def write_config(self, *, path: str, enabled: bool) -> SkillsConfigWriteResult:
361
398
  return self._run(self._async_client.write_config(path=path, enabled=enabled))
362
399
 
400
+ def write_skill(
401
+ self,
402
+ *,
403
+ name: str,
404
+ directory: str,
405
+ instructions: str | bytes,
406
+ reload_cwds: Sequence[str] | None = None,
407
+ ) -> protocol.SkillUserInput:
408
+ return self._run(
409
+ self._async_client.write_skill(
410
+ name=name,
411
+ directory=directory,
412
+ instructions=instructions,
413
+ reload_cwds=reload_cwds,
414
+ )
415
+ )
416
+
417
+
418
+ class _FsClient(_SyncRunner):
419
+ def __init__(
420
+ self,
421
+ async_client: _AsyncFsClientLike,
422
+ runner: Callable[[Coroutine[Any, Any, Any]], Any],
423
+ ) -> None:
424
+ super().__init__(runner)
425
+ self._async_client = async_client
426
+
427
+ def create_directory(
428
+ self,
429
+ *,
430
+ path: str,
431
+ recursive: bool | None = True,
432
+ ) -> protocol.FsCreateDirectoryResponse:
433
+ return self._run(self._async_client.create_directory(path=path, recursive=recursive))
434
+
435
+ def write_file(
436
+ self,
437
+ *,
438
+ path: str,
439
+ data: str | bytes,
440
+ encoding: str = "utf-8",
441
+ ) -> protocol.FsWriteFileResponse:
442
+ return self._run(self._async_client.write_file(path=path, data=data, encoding=encoding))
443
+
363
444
 
364
445
  class _AccountClient(_SyncRunner):
365
446
  def __init__(
@@ -554,6 +635,7 @@ class _CommandClient(_SyncRunner):
554
635
  disable_timeout: bool | None = None,
555
636
  env: Mapping[str, object | None] | None = None,
556
637
  output_bytes_cap: int | None = None,
638
+ permission_profile: str | None = None,
557
639
  process_id: str | None = None,
558
640
  sandbox_policy: protocol.SandboxPolicy | None = None,
559
641
  size: protocol.CommandExecTerminalSize | None = None,
@@ -570,6 +652,7 @@ class _CommandClient(_SyncRunner):
570
652
  disable_timeout=disable_timeout,
571
653
  env=env,
572
654
  output_bytes_cap=output_bytes_cap,
655
+ permission_profile=permission_profile,
573
656
  process_id=process_id,
574
657
  sandbox_policy=sandbox_policy,
575
658
  size=size,