code-analysis-client 1.6.173__tar.gz → 1.6.177__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 (26) hide show
  1. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/PKG-INFO +1 -1
  2. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/__init__.py +3 -0
  3. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/client.py +6 -0
  4. code_analysis_client-1.6.177/code_analysis_client/version.txt +1 -0
  5. code_analysis_client-1.6.177/code_analysis_client/worktree.py +533 -0
  6. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client.egg-info/PKG-INFO +1 -1
  7. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client.egg-info/SOURCES.txt +1 -0
  8. code_analysis_client-1.6.173/code_analysis_client/version.txt +0 -1
  9. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/README.md +0 -0
  10. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/commands_proxy.py +0 -0
  11. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/config.py +0 -0
  12. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/exceptions.py +0 -0
  13. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/file_session.py +0 -0
  14. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/py.typed +0 -0
  15. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/queue_wait.py +0 -0
  16. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/responses.py +0 -0
  17. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/schema_disk_cache.py +0 -0
  18. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/server_api.py +0 -0
  19. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/server_schema.py +0 -0
  20. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/universal_file.py +0 -0
  21. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client/validation.py +0 -0
  22. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client.egg-info/dependency_links.txt +0 -0
  23. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client.egg-info/requires.txt +0 -0
  24. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/code_analysis_client.egg-info/top_level.txt +0 -0
  25. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/pyproject.toml +0 -0
  26. {code_analysis_client-1.6.173 → code_analysis_client-1.6.177}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.6.173
3
+ Version: 1.6.177
4
4
  Summary: Async JSON-RPC client for the code-analysis MCP server (mcp-proxy-adapter JsonRpcClient)
5
5
  Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -24,6 +24,7 @@ from code_analysis_client.exceptions import (
24
24
  QueueJobError,
25
25
  )
26
26
  from code_analysis_client.file_session import FileSessionClient, SessionNotFoundError
27
+ from code_analysis_client.worktree import WorktreeClient, WorktreeConflictError
27
28
  from code_analysis_client.queue_wait import (
28
29
  QueuedJob,
29
30
  extract_job_id,
@@ -71,6 +72,8 @@ __all__ = [
71
72
  "UNIVERSAL_FILE_COMMANDS",
72
73
  "UniversalFileClient",
73
74
  "ValidatedCommandsProxy",
75
+ "WorktreeClient",
76
+ "WorktreeConflictError",
74
77
  "adapter_settings_from_server_config",
75
78
  "adapter_settings_to_jsonrpc_kwargs",
76
79
  "extract_job_id",
@@ -23,6 +23,7 @@ from code_analysis_client.config import (
23
23
  load_server_config,
24
24
  )
25
25
  from code_analysis_client.file_session import FileSessionClient
26
+ from code_analysis_client.worktree import WorktreeClient
26
27
  from code_analysis_client.queue_wait import (
27
28
  QueuedJob,
28
29
  StatusHook,
@@ -147,6 +148,11 @@ class CodeAnalysisAsyncClient:
147
148
  """Session workflow: ``session_*``, ``subordinate_session_*``, transfer, advisory locks."""
148
149
  return FileSessionClient(self)
149
150
 
151
+ @property
152
+ def worktrees(self) -> WorktreeClient:
153
+ """Worktree management: ``worktree_*`` commands."""
154
+ return WorktreeClient(self)
155
+
150
156
  @property
151
157
  def universal_files(self) -> UniversalFileClient:
152
158
  """Read-only structured preview (``universal_file_preview`` command)."""
@@ -0,0 +1,533 @@
1
+ """
2
+ Worktree management on top of CodeAnalysisAsyncClient.
3
+
4
+ Wraps ``worktree_*`` MCP commands with typed client methods, queued and synchronous,
5
+ following the FileSessionClient pattern.
6
+
7
+ Author: Vasiliy Zdanovskiy
8
+ email: vasilyvz@gmail.com
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
14
+
15
+ from code_analysis_client.exceptions import ClientValidationError
16
+ from code_analysis_client.responses import unwrap_command_result
17
+
18
+ if TYPE_CHECKING:
19
+ from code_analysis_client.client import CodeAnalysisAsyncClient
20
+
21
+
22
+ class WorktreeConflictError(ClientValidationError):
23
+ """Raised when worktree_sync encounters WORKTREE_SYNC_CONFLICT."""
24
+
25
+ def __init__(
26
+ self,
27
+ message: str,
28
+ *,
29
+ conflicts: Optional[List[Dict[str, Any]]] = None,
30
+ preserved_locked_files: Optional[List[str]] = None,
31
+ ) -> None:
32
+ """Initialize with conflict details."""
33
+ super().__init__(message, field="worktree_sync")
34
+ self.conflicts = conflicts or []
35
+ self.preserved_locked_files = preserved_locked_files or []
36
+
37
+
38
+ def _unwrap(data: Dict[str, Any]) -> Dict[str, Any]:
39
+ """Unwrap command result payloads."""
40
+ return unwrap_command_result(data)
41
+
42
+
43
+ def _require_non_empty(value: Any, *, field: str) -> str:
44
+ """Return stripped text or raise ClientValidationError for an empty field."""
45
+ text = str(value or "").strip()
46
+ if not text:
47
+ raise ClientValidationError(f"{field} is required", field=field)
48
+ return text
49
+
50
+
51
+ class WorktreeClient:
52
+ """Worktree lifecycle and sync management commands."""
53
+
54
+ __slots__ = ("_client",)
55
+
56
+ def __init__(self, client: CodeAnalysisAsyncClient) -> None:
57
+ """Store the underlying async client used to call worktree commands."""
58
+ self._client = client
59
+
60
+ # === Lifecycle commands ===
61
+
62
+ async def ensure(
63
+ self,
64
+ project_id: str,
65
+ name: str,
66
+ *,
67
+ branch: Optional[str] = None,
68
+ start_point: Optional[str] = None,
69
+ create_branch: bool = True,
70
+ owner_session_id: Optional[str] = None,
71
+ ) -> Dict[str, Any]:
72
+ """Create a worktree if absent; idempotent if already present with same branch.
73
+
74
+ Calls ``worktree_ensure`` per candidate-WT-1.md section 5.1.
75
+ """
76
+ project_id = _require_non_empty(project_id, field="project_id")
77
+ name = _require_non_empty(name, field="name")
78
+
79
+ params: Dict[str, Any] = {
80
+ "project_id": project_id,
81
+ "name": name,
82
+ }
83
+ if branch is not None:
84
+ params["branch"] = branch
85
+ if start_point is not None:
86
+ params["start_point"] = start_point
87
+ if not create_branch:
88
+ params["create_branch"] = False
89
+ if owner_session_id is not None:
90
+ params["owner_session_id"] = owner_session_id
91
+
92
+ return _unwrap(
93
+ await self._client.call_validated("worktree_ensure", params)
94
+ )
95
+
96
+ async def list(
97
+ self,
98
+ project_id: str,
99
+ *,
100
+ owner_session_id: Optional[str] = None,
101
+ only_names: Optional[List[str]] = None,
102
+ ) -> Dict[str, Any]:
103
+ """List worktrees in a project.
104
+
105
+ Calls ``worktree_list`` per candidate-WT-1.md section 5.2.
106
+ """
107
+ project_id = _require_non_empty(project_id, field="project_id")
108
+
109
+ params: Dict[str, Any] = {"project_id": project_id}
110
+ if owner_session_id is not None:
111
+ params["owner_session_id"] = owner_session_id
112
+ if only_names is not None:
113
+ params["only_names"] = only_names
114
+
115
+ return _unwrap(
116
+ await self._client.call_validated("worktree_list", params)
117
+ )
118
+
119
+ async def status(
120
+ self,
121
+ project_id: str,
122
+ name: str,
123
+ ) -> Dict[str, Any]:
124
+ """Get git status of one worktree (working tree + index).
125
+
126
+ Calls ``worktree_status`` per candidate-WT-1.md section 5.3.
127
+ """
128
+ project_id = _require_non_empty(project_id, field="project_id")
129
+ name = _require_non_empty(name, field="name")
130
+
131
+ params: Dict[str, Any] = {
132
+ "project_id": project_id,
133
+ "name": name,
134
+ }
135
+
136
+ return _unwrap(
137
+ await self._client.call_validated("worktree_status", params)
138
+ )
139
+
140
+ async def diff(
141
+ self,
142
+ project_id: str,
143
+ name: str,
144
+ *,
145
+ base: Optional[str] = None,
146
+ path: Optional[str] = None,
147
+ include_patch: bool = False,
148
+ ) -> Dict[str, Any]:
149
+ """Diff worktree against base branch.
150
+
151
+ Calls ``worktree_diff`` per candidate-WT-1.md section 5.4.
152
+ """
153
+ project_id = _require_non_empty(project_id, field="project_id")
154
+ name = _require_non_empty(name, field="name")
155
+
156
+ params: Dict[str, Any] = {
157
+ "project_id": project_id,
158
+ "name": name,
159
+ }
160
+ if base is not None:
161
+ params["base"] = base
162
+ if path is not None:
163
+ params["path"] = path
164
+ if include_patch:
165
+ params["include_patch"] = True
166
+
167
+ return _unwrap(
168
+ await self._client.call_validated("worktree_diff", params)
169
+ )
170
+
171
+ # === Sync and conflict resolution ===
172
+
173
+ async def sync(
174
+ self,
175
+ project_id: str,
176
+ name: str,
177
+ *,
178
+ strategy: Optional[str] = None,
179
+ base: Optional[str] = None,
180
+ auto_poll: bool = True,
181
+ timeout: Optional[float] = None,
182
+ ) -> Dict[str, Any]:
183
+ """Sync worktree from main branch (merge or rebase).
184
+
185
+ Calls ``worktree_sync`` per candidate-WT-1.md section 5.5.
186
+ Raises WorktreeConflictError on WORKTREE_SYNC_CONFLICT.
187
+ """
188
+ project_id = _require_non_empty(project_id, field="project_id")
189
+ name = _require_non_empty(name, field="name")
190
+
191
+ params: Dict[str, Any] = {
192
+ "project_id": project_id,
193
+ "name": name,
194
+ }
195
+ if strategy is not None:
196
+ params["strategy"] = strategy
197
+ if base is not None:
198
+ params["base"] = base
199
+
200
+ result = await self._client.call_validated(
201
+ "worktree_sync",
202
+ params,
203
+ auto_poll=auto_poll,
204
+ timeout=timeout,
205
+ )
206
+
207
+ # Check for WORKTREE_SYNC_CONFLICT error
208
+ if isinstance(result, dict) and not result.get("success", True):
209
+ error = result.get("error", {})
210
+ if error.get("code") == "WORKTREE_SYNC_CONFLICT":
211
+ details = error.get("details", {})
212
+ raise WorktreeConflictError(
213
+ "Worktree sync conflict",
214
+ conflicts=details.get("conflicts"),
215
+ preserved_locked_files=details.get("preserved_locked_files"),
216
+ )
217
+
218
+ return _unwrap(result)
219
+
220
+ async def conflicts(
221
+ self,
222
+ project_id: str,
223
+ name: str,
224
+ ) -> Dict[str, Any]:
225
+ """List merge/rebase conflicts with per-file markers.
226
+
227
+ Calls ``worktree_conflicts`` per candidate-WT-1.md section 5.6.
228
+ """
229
+ project_id = _require_non_empty(project_id, field="project_id")
230
+ name = _require_non_empty(name, field="name")
231
+
232
+ params: Dict[str, Any] = {
233
+ "project_id": project_id,
234
+ "name": name,
235
+ }
236
+
237
+ return _unwrap(
238
+ await self._client.call_validated("worktree_conflicts", params)
239
+ )
240
+
241
+ async def continue_(
242
+ self,
243
+ project_id: str,
244
+ name: str,
245
+ resolved_paths: List[str],
246
+ *,
247
+ message: Optional[str] = None,
248
+ auto_poll: bool = True,
249
+ timeout: Optional[float] = None,
250
+ ) -> Dict[str, Any]:
251
+ """Finalize merge or rebase after resolving conflicts.
252
+
253
+ Calls ``worktree_continue`` per candidate-WT-1.md section 5.7.
254
+ """
255
+ project_id = _require_non_empty(project_id, field="project_id")
256
+ name = _require_non_empty(name, field="name")
257
+ if not resolved_paths:
258
+ raise ClientValidationError(
259
+ "resolved_paths must be non-empty",
260
+ field="resolved_paths",
261
+ )
262
+
263
+ params: Dict[str, Any] = {
264
+ "project_id": project_id,
265
+ "name": name,
266
+ "resolved_paths": resolved_paths,
267
+ }
268
+ if message is not None:
269
+ params["message"] = message
270
+
271
+ return _unwrap(
272
+ await self._client.call_validated(
273
+ "worktree_continue",
274
+ params,
275
+ auto_poll=auto_poll,
276
+ timeout=timeout,
277
+ )
278
+ )
279
+
280
+ async def abort(
281
+ self,
282
+ project_id: str,
283
+ name: str,
284
+ *,
285
+ auto_poll: bool = True,
286
+ timeout: Optional[float] = None,
287
+ ) -> Dict[str, Any]:
288
+ """Abort in-progress merge or rebase.
289
+
290
+ Calls ``worktree_abort`` per candidate-WT-1.md section 5.8.
291
+ """
292
+ project_id = _require_non_empty(project_id, field="project_id")
293
+ name = _require_non_empty(name, field="name")
294
+
295
+ params: Dict[str, Any] = {
296
+ "project_id": project_id,
297
+ "name": name,
298
+ }
299
+
300
+ return _unwrap(
301
+ await self._client.call_validated(
302
+ "worktree_abort",
303
+ params,
304
+ auto_poll=auto_poll,
305
+ timeout=timeout,
306
+ )
307
+ )
308
+
309
+ # === Commit and removal ===
310
+
311
+ async def commit(
312
+ self,
313
+ project_id: str,
314
+ name: str,
315
+ message: str,
316
+ *,
317
+ paths: Optional[List[str]] = None,
318
+ all: bool = False,
319
+ auto_poll: bool = True,
320
+ timeout: Optional[float] = None,
321
+ ) -> Dict[str, Any]:
322
+ """Commit staged or all changes in worktree.
323
+
324
+ Calls ``worktree_commit`` per candidate-WT-1.md section 5.9.
325
+ """
326
+ project_id = _require_non_empty(project_id, field="project_id")
327
+ name = _require_non_empty(name, field="name")
328
+ message = _require_non_empty(message, field="message")
329
+
330
+ if not paths and not all:
331
+ raise ClientValidationError(
332
+ "At least one of paths or all=true is required",
333
+ field="paths",
334
+ )
335
+
336
+ params: Dict[str, Any] = {
337
+ "project_id": project_id,
338
+ "name": name,
339
+ "message": message,
340
+ }
341
+ if paths is not None:
342
+ params["paths"] = paths
343
+ if all:
344
+ params["all"] = True
345
+
346
+ return _unwrap(
347
+ await self._client.call_validated(
348
+ "worktree_commit",
349
+ params,
350
+ auto_poll=auto_poll,
351
+ timeout=timeout,
352
+ )
353
+ )
354
+
355
+ async def remove(
356
+ self,
357
+ project_id: str,
358
+ name: str,
359
+ *,
360
+ force: bool = False,
361
+ delete_branch: bool = True,
362
+ auto_poll: bool = True,
363
+ timeout: Optional[float] = None,
364
+ ) -> Dict[str, Any]:
365
+ """Remove a worktree and optionally its branch.
366
+
367
+ Calls ``worktree_remove`` per candidate-WT-1.md section 5.10.
368
+ """
369
+ project_id = _require_non_empty(project_id, field="project_id")
370
+ name = _require_non_empty(name, field="name")
371
+
372
+ params: Dict[str, Any] = {
373
+ "project_id": project_id,
374
+ "name": name,
375
+ }
376
+ if force:
377
+ params["force"] = True
378
+ if not delete_branch:
379
+ params["delete_branch"] = False
380
+
381
+ return _unwrap(
382
+ await self._client.call_validated(
383
+ "worktree_remove",
384
+ params,
385
+ auto_poll=auto_poll,
386
+ timeout=timeout,
387
+ )
388
+ )
389
+
390
+ async def prune(
391
+ self,
392
+ project_id: str,
393
+ *,
394
+ name: Optional[str] = None,
395
+ ttl_seconds: Optional[int] = None,
396
+ only_names: Optional[List[str]] = None,
397
+ dry_run: bool = False,
398
+ auto_poll: bool = True,
399
+ timeout: Optional[float] = None,
400
+ ) -> Dict[str, Any]:
401
+ """TTL safety net: reap idle worktrees whose sessions are gone.
402
+
403
+ Calls ``worktree_prune`` per candidate-WT-1.md section 5.11.
404
+ """
405
+ project_id = _require_non_empty(project_id, field="project_id")
406
+
407
+ params: Dict[str, Any] = {"project_id": project_id}
408
+ if name is not None:
409
+ params["name"] = name
410
+ if ttl_seconds is not None:
411
+ params["ttl_seconds"] = ttl_seconds
412
+ if only_names is not None:
413
+ params["only_names"] = only_names
414
+ if dry_run:
415
+ params["dry_run"] = True
416
+
417
+ return _unwrap(
418
+ await self._client.call_validated(
419
+ "worktree_prune",
420
+ params,
421
+ auto_poll=auto_poll,
422
+ timeout=timeout,
423
+ )
424
+ )
425
+
426
+ # === Lock and maintenance ===
427
+
428
+ async def lock(
429
+ self,
430
+ project_id: str,
431
+ name: str,
432
+ *,
433
+ reason: Optional[str] = None,
434
+ ) -> Dict[str, Any]:
435
+ """Lock a worktree to protect from removal.
436
+
437
+ Calls ``worktree_lock`` per candidate-WT-1.md section 5.12.
438
+ """
439
+ project_id = _require_non_empty(project_id, field="project_id")
440
+ name = _require_non_empty(name, field="name")
441
+
442
+ params: Dict[str, Any] = {
443
+ "project_id": project_id,
444
+ "name": name,
445
+ }
446
+ if reason is not None:
447
+ params["reason"] = reason
448
+
449
+ return _unwrap(
450
+ await self._client.call_validated("worktree_lock", params)
451
+ )
452
+
453
+ async def unlock(
454
+ self,
455
+ project_id: str,
456
+ name: str,
457
+ ) -> Dict[str, Any]:
458
+ """Unlock a worktree.
459
+
460
+ Calls ``worktree_unlock`` per candidate-WT-1.md section 5.13.
461
+ """
462
+ project_id = _require_non_empty(project_id, field="project_id")
463
+ name = _require_non_empty(name, field="name")
464
+
465
+ params: Dict[str, Any] = {
466
+ "project_id": project_id,
467
+ "name": name,
468
+ }
469
+
470
+ return _unwrap(
471
+ await self._client.call_validated("worktree_unlock", params)
472
+ )
473
+
474
+ async def move(
475
+ self,
476
+ project_id: str,
477
+ name: str,
478
+ new_name: str,
479
+ *,
480
+ auto_poll: bool = True,
481
+ timeout: Optional[float] = None,
482
+ ) -> Dict[str, Any]:
483
+ """Move/rename a worktree.
484
+
485
+ Calls ``worktree_move`` per candidate-WT-1.md section 5.14.
486
+ """
487
+ project_id = _require_non_empty(project_id, field="project_id")
488
+ name = _require_non_empty(name, field="name")
489
+ new_name = _require_non_empty(new_name, field="new_name")
490
+
491
+ params: Dict[str, Any] = {
492
+ "project_id": project_id,
493
+ "name": name,
494
+ "new_name": new_name,
495
+ }
496
+
497
+ return _unwrap(
498
+ await self._client.call_validated(
499
+ "worktree_move",
500
+ params,
501
+ auto_poll=auto_poll,
502
+ timeout=timeout,
503
+ )
504
+ )
505
+
506
+ async def repair(
507
+ self,
508
+ project_id: str,
509
+ name: str,
510
+ *,
511
+ auto_poll: bool = True,
512
+ timeout: Optional[float] = None,
513
+ ) -> Dict[str, Any]:
514
+ """Repair worktree after manual filesystem move/restore.
515
+
516
+ Calls ``worktree_repair`` per candidate-WT-1.md section 5.15.
517
+ """
518
+ project_id = _require_non_empty(project_id, field="project_id")
519
+ name = _require_non_empty(name, field="name")
520
+
521
+ params: Dict[str, Any] = {
522
+ "project_id": project_id,
523
+ "name": name,
524
+ }
525
+
526
+ return _unwrap(
527
+ await self._client.call_validated(
528
+ "worktree_repair",
529
+ params,
530
+ auto_poll=auto_poll,
531
+ timeout=timeout,
532
+ )
533
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-analysis-client
3
- Version: 1.6.173
3
+ Version: 1.6.177
4
4
  Summary: Async JSON-RPC client for the code-analysis MCP server (mcp-proxy-adapter JsonRpcClient)
5
5
  Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -15,6 +15,7 @@ code_analysis_client/server_schema.py
15
15
  code_analysis_client/universal_file.py
16
16
  code_analysis_client/validation.py
17
17
  code_analysis_client/version.txt
18
+ code_analysis_client/worktree.py
18
19
  code_analysis_client.egg-info/PKG-INFO
19
20
  code_analysis_client.egg-info/SOURCES.txt
20
21
  code_analysis_client.egg-info/dependency_links.txt