python-codex 0.2.2__py3-none-any.whl → 0.2.4__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.
pycodex/cli.py CHANGED
@@ -165,6 +165,7 @@ def resolve_prompt_text(prompt_parts: 'Sequence[str]') -> 'str':
165
165
  def get_tools(
166
166
  runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None,
167
167
  exec_mode: 'bool' = False,
168
+ cwd: 'typing.Union[str, Path, None]' = None,
168
169
  ):
169
170
  from .tools import (
170
171
  ApplyPatchTool,
@@ -194,8 +195,8 @@ def get_tools(
194
195
 
195
196
  runtime_environment = runtime_environment or create_agent_runtime_environment()
196
197
  registry = Registry()
197
- code_mode_manager = CodeModeManager(registry)
198
- unified_exec_manager = UnifiedExecManager()
198
+ code_mode_manager = CodeModeManager(registry, cwd=cwd)
199
+ unified_exec_manager = UnifiedExecManager(cwd=cwd)
199
200
  exec_tool = ExecTool(code_mode_manager)
200
201
  wait_tool = WaitTool(code_mode_manager)
201
202
  web_search_tool = WebSearchTool()
@@ -211,15 +212,15 @@ def get_tools(
211
212
  resume_agent_tool = ResumeAgentTool(runtime_environment.subagent_manager)
212
213
  wait_agent_tool = WaitAgentTool(runtime_environment.subagent_manager)
213
214
  close_agent_tool = CloseAgentTool(runtime_environment.subagent_manager)
214
- apply_patch_tool = ApplyPatchTool()
215
- shell_tool = ShellTool()
216
- shell_command_tool = ShellCommandTool()
215
+ apply_patch_tool = ApplyPatchTool(cwd=cwd)
216
+ shell_tool = ShellTool(cwd=cwd)
217
+ shell_command_tool = ShellCommandTool(cwd=cwd)
217
218
  exec_command_tool = ExecCommandTool(unified_exec_manager)
218
219
  write_stdin_tool = WriteStdinTool(unified_exec_manager)
219
- grep_files_tool = GrepFilesTool()
220
+ grep_files_tool = GrepFilesTool(cwd=cwd)
220
221
  read_file_tool = ReadFileTool()
221
222
  list_dir_tool = ListDirTool()
222
- view_image_tool = ViewImageTool()
223
+ view_image_tool = ViewImageTool(cwd=cwd)
223
224
  if exec_mode:
224
225
  registry.register(exec_command_tool)
225
226
  registry.register(write_stdin_tool)
@@ -258,7 +259,10 @@ def get_tools(
258
259
  return registry
259
260
 
260
261
 
261
- def get_subagent_tools(runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None):
262
+ def get_subagent_tools(
263
+ runtime_environment: 'typing.Union[AgentRuntimeEnvironment, None]' = None,
264
+ cwd: 'typing.Union[str, Path, None]' = None,
265
+ ):
262
266
  from .tools import (
263
267
  ApplyPatchTool,
264
268
  ExecCommandTool,
@@ -272,13 +276,13 @@ def get_subagent_tools(runtime_environment: 'typing.Union[AgentRuntimeEnvironmen
272
276
 
273
277
  runtime_environment = runtime_environment or create_agent_runtime_environment()
274
278
  registry = Registry()
275
- unified_exec_manager = UnifiedExecManager()
279
+ unified_exec_manager = UnifiedExecManager(cwd=cwd)
276
280
  registry.register(ExecCommandTool(unified_exec_manager))
277
281
  registry.register(WriteStdinTool(unified_exec_manager))
278
282
  registry.register(UpdatePlanTool(runtime_environment.plan_store))
279
- registry.register(ApplyPatchTool())
283
+ registry.register(ApplyPatchTool(cwd=cwd))
280
284
  registry.register(WebSearchTool())
281
- registry.register(ViewImageTool())
285
+ registry.register(ViewImageTool(cwd=cwd))
282
286
  return registry
283
287
 
284
288
 
@@ -290,8 +294,10 @@ def build_agent(
290
294
  session_mode: 'CliSessionMode' = "exec",
291
295
  collaboration_mode: 'CollaborationMode' = DEFAULT_COLLABORATION_MODE,
292
296
  extra_contextual_user_messages: 'typing.Iterable[str]' = (),
297
+ cwd: 'typing.Union[str, Path, None]' = None,
293
298
  ) -> 'Agent':
294
299
  config_path = str(config_path)
300
+ resolved_cwd = Path(cwd or Path.cwd()).resolve()
295
301
  context_manager = ContextManager.from_codex_config(
296
302
  config_path,
297
303
  profile,
@@ -299,6 +305,7 @@ def build_agent(
299
305
  collaboration_mode=collaboration_mode,
300
306
  include_collaboration_instructions=session_mode == "tui",
301
307
  extra_contextual_user_messages=extra_contextual_user_messages,
308
+ cwd=resolved_cwd,
302
309
  )
303
310
  session_id = getattr(client, "_session_id", None) or uuid7_string()
304
311
  if hasattr(client, "_session_id"):
@@ -309,6 +316,7 @@ def build_agent(
309
316
  base_instructions_override=system_prompt,
310
317
  include_collaboration_instructions=False,
311
318
  extra_contextual_user_messages=extra_contextual_user_messages,
319
+ cwd=resolved_cwd,
312
320
  )
313
321
  runtime_environment = create_agent_runtime_environment()
314
322
  runtime_environment.request_user_input_manager.set_handler(None)
@@ -343,7 +351,7 @@ def build_agent(
343
351
  )
344
352
  sub_agent = Agent(
345
353
  nested_client,
346
- get_subagent_tools(subagent_agent_runtime_environment),
354
+ get_subagent_tools(subagent_agent_runtime_environment, cwd=resolved_cwd),
347
355
  subagent_context_manager,
348
356
  initial_history=tuple(initial_history),
349
357
  runtime_environment=subagent_agent_runtime_environment,
@@ -357,7 +365,7 @@ def build_agent(
357
365
  )
358
366
  return Agent(
359
367
  client,
360
- get_tools(runtime_environment, exec_mode=True),
368
+ get_tools(runtime_environment, exec_mode=True, cwd=resolved_cwd),
361
369
  context_manager,
362
370
  rollout_recorder=rollout_recorder,
363
371
  runtime_environment=runtime_environment,
pycodex/context.py CHANGED
@@ -150,8 +150,9 @@ class ContextManager:
150
150
  include_skills_instructions: 'bool' = True,
151
151
  network_access: 'str' = "enabled",
152
152
  extra_contextual_user_messages: 'typing.Iterable[str]' = (),
153
+ cwd: 'typing.Union[str, Path, None]' = None,
153
154
  ) -> 'None':
154
- self.cwd = Path.cwd().resolve()
155
+ self.cwd = Path(cwd or Path.cwd()).resolve()
155
156
  self._shell = get_shell_name()
156
157
  self._current_date = datetime.now().date().isoformat()
157
158
  self._timezone_name = get_timezone_name()
@@ -193,6 +194,7 @@ class ContextManager:
193
194
  include_skills_instructions: 'bool' = True,
194
195
  network_access: 'str' = "enabled",
195
196
  extra_contextual_user_messages: 'typing.Iterable[str]' = (),
197
+ cwd: 'typing.Union[str, Path, None]' = None,
196
198
  ) -> 'ContextManager':
197
199
  config = ContextConfig.from_codex_config(config_path, profile)
198
200
  return cls(
@@ -204,6 +206,7 @@ class ContextManager:
204
206
  include_skills_instructions=include_skills_instructions,
205
207
  network_access=network_access,
206
208
  extra_contextual_user_messages=extra_contextual_user_messages,
209
+ cwd=cwd,
207
210
  )
208
211
 
209
212
  @property
pycodex/feishu_link.py CHANGED
@@ -224,7 +224,8 @@ class _FeishuCardActionListener:
224
224
  self.domain = card.domain
225
225
  self.encrypt_key = card.encrypt_key
226
226
  self.verification_token = card.verification_token
227
- self.link = None
227
+ self._links = {}
228
+ self._links_lock = threading.Lock()
228
229
  self._async_thread = _AsyncLoopThread()
229
230
  self._thread = None
230
231
  self._client = None
@@ -250,14 +251,29 @@ class _FeishuCardActionListener:
250
251
  self._thread.start()
251
252
 
252
253
  def register(self, link: PycodexRuntimeLink) -> None:
253
- self.link = link
254
+ message_id = str(getattr(link, "message_id", "") or "").strip()
255
+ if not message_id:
256
+ raise RuntimeError("Feishu linked card message_id is required")
257
+ with self._links_lock:
258
+ self._links[message_id] = link
254
259
 
255
260
  def unregister(self, link: PycodexRuntimeLink) -> None:
256
- if self.link is link:
257
- self.link = None
261
+ message_id = str(getattr(link, "message_id", "") or "").strip()
262
+ with self._links_lock:
263
+ if message_id and self._links.get(message_id) is link:
264
+ del self._links[message_id]
265
+ return
266
+ stale_ids = [
267
+ registered_id
268
+ for registered_id, registered_link in self._links.items()
269
+ if registered_link is link
270
+ ]
271
+ for registered_id in stale_ids:
272
+ del self._links[registered_id]
258
273
 
259
274
  def empty(self) -> bool:
260
- return self.link is None
275
+ with self._links_lock:
276
+ return not self._links
261
277
 
262
278
  def stop(self) -> None:
263
279
  self._stop_requested.set()
@@ -329,16 +345,9 @@ class _FeishuCardActionListener:
329
345
 
330
346
  def _handle_card_action(self, event) -> 'typing.Dict[str, object]':
331
347
  try:
332
- link = self.link
333
- if link is None or link._detached:
334
- return {
335
- "toast": {
336
- "type": "warning",
337
- "content": "pycodex is detached.",
338
- }
339
- }
340
348
  message_id = _event_message_id(event)
341
- if message_id and message_id != link.message_id:
349
+ link = self._resolve_link(message_id)
350
+ if link is None or link._detached:
342
351
  return {
343
352
  "toast": {
344
353
  "type": "warning",
@@ -355,6 +364,15 @@ class _FeishuCardActionListener:
355
364
  }
356
365
  }
357
366
 
367
+ def _resolve_link(self, message_id):
368
+ message_id = str(message_id or "").strip()
369
+ with self._links_lock:
370
+ if message_id:
371
+ return self._links.get(message_id)
372
+ if len(self._links) == 1:
373
+ return next(iter(self._links.values()))
374
+ return None
375
+
358
376
 
359
377
  def _event_message_id(event) -> "typing.Union[str, None]":
360
378
  event_data = getattr(event, "event", None)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A minimal Python extraction of Codex's main agent loop
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.6.2
@@ -165,15 +165,16 @@ pycodex --put @127.0.0.1:5577
165
165
  pycodex --put /data/.codex/@127.0.0.1:5577
166
166
  pycodex --call SECRET-CALLID@127.0.0.1:5577 "Reply with exactly OK."
167
167
  pycodex doctor
168
- pycodex-ws --listen 0.0.0.0:6007 --board ./board.html
168
+ pycodex-ws --listen 0.0.0.0:6007 --workspace-config ./workspaces.json
169
+ pycodex-ws --listen 0.0.0.0:6007 --workspace-config ./workspaces.json --password 12345
169
170
  ```
170
171
 
171
172
  Current behavior:
172
173
 
173
174
  - with no argv prompt and a TTY stdin, enter interactive mode
174
175
  - with an argv prompt or piped stdin, run a single turn
175
- - `pycodex-ws` starts the standalone browser workspace with a board pane and a
176
- pycodex session pane
176
+ - `pycodex-ws` starts the standalone browser workspace manager and serves each
177
+ workspace with a board pane and a pycodex session pane
177
178
  - interactive mode supports `/exit` and `/quit`
178
179
  - interactive mode shows a compact event stream for user-visible phases such as
179
180
  tool execution and model follow-up after tool results
@@ -204,6 +205,22 @@ Current behavior:
204
205
  non-persisted follow-up session and submits the file contents as the next
205
206
  user instruction; this is intended for side-effect follow-ups such as
206
207
  Feishu notifications
208
+ - `/link <feishu-email|open_id|chat_id>` attaches the current interactive
209
+ session to a Feishu card; multiple sessions in the same pycodex process share
210
+ one Feishu long-connection listener and route card actions by message id
211
+ - `pycodex-ws --workspace-config workspaces.json` serves workspace boards from
212
+ one process. The JSON file can be either a list or
213
+ `{"workspaces": [...]}`; each entry uses `board` and `work_dir`, with optional
214
+ display/URL `id`, and is exposed at `/w/<id>/`. Internally the resolved
215
+ `board` path is the workspace identity, so adding the same board path twice is
216
+ rejected even with a different name. Relative `board` and `work_dir` paths
217
+ resolve from the config file directory. The root path `/` shows a workspaces
218
+ management page with `add_workspace(name=None, dir="./", board=None)` and
219
+ `delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
220
+ etc. If `board` is omitted when adding a workspace, pycodex assigns a random
221
+ writable `/tmp/pcws-*.html` board path. Add/delete actions and later
222
+ session-state saves refresh the JSON file. `--password <value>` enables a
223
+ password-only login page for workspace pages, APIs, and websocket connections.
207
224
  - steer is enabled by default in interactive mode: normal input goes into the
208
225
  runtime steer path, the current request stops at the next safe boundary, and
209
226
  later steer text is appended to the next model request's `input` in order;
@@ -1,12 +1,12 @@
1
1
  pycodex/__init__.py,sha256=I1P7OHkabjHP7g32A31o-L9NMd3lKYJ2xhV8AHLPYXA,3196
2
2
  pycodex/agent.py,sha256=HKzWVFRag_Le8LVR1qqfdfVzk23bRsXkykZV8Zq2hLE,21659
3
- pycodex/cli.py,sha256=6URw4uSV-GPevu8NiqoraWIa180QmSUFOHScCZHO2Mc,20616
3
+ pycodex/cli.py,sha256=1wTlt3ccgQqczrWBV3IjUVE0_L2QUlfPZEf6GB-tGZY,20983
4
4
  pycodex/collaboration.py,sha256=yQ6pBD-R3ZWR4_FAYQFoS7KF0m4LLD42otXIbPqw2ys,641
5
5
  pycodex/compat.py,sha256=l35JE0vGAOCn9NWbWbqwGURGk83HXddXQ5wJIfcG41o,3254
6
- pycodex/context.py,sha256=Hwh3vU-qSvaleUDdOWuWiGyNmbULVAZmboxiu9iiHt8,26549
6
+ pycodex/context.py,sha256=uI8y7V7L3VsXuDBljXWhP5tQRJM6MYQtoeEu2oSIJb4,26689
7
7
  pycodex/doctor.py,sha256=De3M4hRBJq8ZeqsUJgHz0vitqrH18YugrEnz7oHhTdQ,10572
8
8
  pycodex/feishu_card.py,sha256=De6pM--3MfhgGo-WcWfhm-fop5UtzjwKrZ4gI2Lls3w,26198
9
- pycodex/feishu_link.py,sha256=XGV93CIorcc03pNgBnXay9SkivnDL4i2GkLR5NjBjq0,15588
9
+ pycodex/feishu_link.py,sha256=DDQCYXQXgo_2SpXPyWlK8KYcumNO1-sqFuQDsEMQpTg,16427
10
10
  pycodex/interactive_session.py,sha256=pcIC_GvQuqQSsjzcTTFB_crrS-XVufAGplJZxnHgy7I,15769
11
11
  pycodex/model.py,sha256=O5Gf35qCMAGrqvr_5d1ZQNk3vHmNRD5Uv-8k7-zbpXg,36679
12
12
  pycodex/portable.py,sha256=gxl2E2h5uZJbasMEPPs-nyALFPIvX79T2ZYsu6vXZrg,15656
@@ -77,12 +77,14 @@ responses_server/trajectory_dump.py,sha256=XCwYaZZmlAxSsSXOfhk3zRvyfDpOHX5R8Kzsp
77
77
  responses_server/tools/__init__.py,sha256=ivsBSEy0SBUhY-Uea5v1XMLXShkwHdCVl0id-1FwdZg,150
78
78
  responses_server/tools/custom_adapter.py,sha256=LxO7ldydvR-GWachDz8GKC0Q8KGGFoFPbZxM0QvxuZ0,8350
79
79
  responses_server/tools/web_search.py,sha256=pm4ZUiHUfxc0bGY1kEvt-BCzDrZIyP24xzPUcga2ul0,8908
80
- workspace_server/__init__.py,sha256=XbSU6aAeYBMsBTVi9Ug6ZW9sxcwefqPS7T_7wpt_VMo,462
80
+ workspace_server/__init__.py,sha256=PRM4ONODb6hxjBUHkBD4TprE_pkTCIlT9sfftK5ic6M,866
81
81
  workspace_server/__main__.py,sha256=9SRp-Yw7ShGxc6DhSIXcDLKgGEdAVm3oBZ59rBOPjT0,62
82
- workspace_server/app.py,sha256=yORhk3BcZZiO1x-QTh0wFi4BXvrS5fbYMgVfEgGBgq8,51270
83
- workspace_server/workspace.html,sha256=5cHWRXGFknkDDqrOtmiBeOI1HRQHbRmkrNFm8wZ4_JU,39202
84
- python_codex-0.2.2.dist-info/METADATA,sha256=GbrTS_Leh6VMxhYAImSrdaFzDblJbwW4Xj5FdCRfdfI,16812
85
- python_codex-0.2.2.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
86
- python_codex-0.2.2.dist-info/entry_points.txt,sha256=vkV2UWCtEKvQNMJuPNjt8HyBKiwp83JyqBatrBNGDp8,80
87
- python_codex-0.2.2.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
88
- python_codex-0.2.2.dist-info/RECORD,,
82
+ workspace_server/app.py,sha256=m1xG4ASJJMnB7mIIKAor43-oze7XFXXdIA8XBbUaclA,55098
83
+ workspace_server/workspace.html,sha256=96uDofM2ZNnNzAhunyy5xPYk-Ii_m73elbiMbZJOAdA,40443
84
+ workspace_server/workspaces.html,sha256=GuYlFiOm1RJINrCKncClHt24D5i7XNABiP9C0xGxBA0,15679
85
+ workspace_server/workspaces.py,sha256=Z-zoj0xgkOOP2CwbQl3vAI6TwLIDUInJKyobs0Jt3ps,19962
86
+ python_codex-0.2.4.dist-info/METADATA,sha256=UgU9tI4s9gFR__d2aFnm4FqR-RPfa0odly3VWFJaWXE,18163
87
+ python_codex-0.2.4.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
88
+ python_codex-0.2.4.dist-info/entry_points.txt,sha256=vkV2UWCtEKvQNMJuPNjt8HyBKiwp83JyqBatrBNGDp8,80
89
+ python_codex-0.2.4.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
90
+ python_codex-0.2.4.dist-info/RECORD,,
@@ -2,22 +2,38 @@ from .app import (
2
2
  ThreadedWorkspaceInteractiveSession,
3
3
  WebSessionView,
4
4
  WorkspaceInteractiveSession,
5
- WorkspaceSessionManager,
6
5
  build_parser,
7
6
  create_app,
7
+ create_multi_workspace_app,
8
8
  main,
9
- parse_target,
9
+ parse_listen,
10
10
  run_serve_cli,
11
11
  )
12
+ from .workspaces import (
13
+ WorkspaceDefinition,
14
+ WorkspaceEntry,
15
+ WorkspaceRegistry,
16
+ WorkspaceSessionManager,
17
+ WorkspaceStateStore,
18
+ default_board_path,
19
+ load_workspace_definitions,
20
+ )
12
21
 
13
22
  __all__ = [
14
23
  "ThreadedWorkspaceInteractiveSession",
15
24
  "WebSessionView",
16
25
  "WorkspaceInteractiveSession",
26
+ "WorkspaceDefinition",
27
+ "WorkspaceEntry",
28
+ "WorkspaceRegistry",
17
29
  "WorkspaceSessionManager",
30
+ "WorkspaceStateStore",
18
31
  "build_parser",
19
32
  "create_app",
33
+ "create_multi_workspace_app",
34
+ "default_board_path",
35
+ "load_workspace_definitions",
20
36
  "main",
21
- "parse_target",
37
+ "parse_listen",
22
38
  "run_serve_cli",
23
39
  ]