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