cortexshift 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.
Files changed (100) hide show
  1. cortexshift/__init__.py +10 -0
  2. cortexshift/__main__.py +6 -0
  3. cortexshift/adapters/__init__.py +22 -0
  4. cortexshift/adapters/command_runner.py +116 -0
  5. cortexshift/adapters/discovery.py +55 -0
  6. cortexshift/adapters/git/__init__.py +10 -0
  7. cortexshift/adapters/git/inspector.py +321 -0
  8. cortexshift/adapters/git/parser.py +140 -0
  9. cortexshift/adapters/headless_runner.py +92 -0
  10. cortexshift/adapters/process_runner.py +56 -0
  11. cortexshift/adapters/providers/__init__.py +4 -0
  12. cortexshift/adapters/providers/antigravity.py +530 -0
  13. cortexshift/adapters/providers/claude.py +375 -0
  14. cortexshift/adapters/providers/codex.py +434 -0
  15. cortexshift/adapters/sqlite/__init__.py +10 -0
  16. cortexshift/adapters/sqlite/migrations.py +268 -0
  17. cortexshift/adapters/sqlite/store.py +914 -0
  18. cortexshift/adapters/workspace_lease.py +123 -0
  19. cortexshift/application/__init__.py +42 -0
  20. cortexshift/application/checkpoint_builder.py +218 -0
  21. cortexshift/application/checkpoint_service.py +273 -0
  22. cortexshift/application/doctor.py +80 -0
  23. cortexshift/application/handoff_builder.py +281 -0
  24. cortexshift/application/handoff_renderer.py +430 -0
  25. cortexshift/application/handoff_service.py +66 -0
  26. cortexshift/application/init_service.py +86 -0
  27. cortexshift/application/locator.py +48 -0
  28. cortexshift/application/native_session.py +65 -0
  29. cortexshift/application/recovery_service.py +235 -0
  30. cortexshift/application/repository_service.py +146 -0
  31. cortexshift/application/resume_service.py +124 -0
  32. cortexshift/application/run_service.py +270 -0
  33. cortexshift/application/session_launcher.py +183 -0
  34. cortexshift/application/session_service.py +63 -0
  35. cortexshift/application/source_session.py +62 -0
  36. cortexshift/application/status_service.py +73 -0
  37. cortexshift/application/switch_service.py +671 -0
  38. cortexshift/application/task_service.py +201 -0
  39. cortexshift/application/task_workspace.py +152 -0
  40. cortexshift/cli/__init__.py +5 -0
  41. cortexshift/cli/app.py +2477 -0
  42. cortexshift/domain/__init__.py +153 -0
  43. cortexshift/domain/checkpoint.py +174 -0
  44. cortexshift/domain/doctor.py +68 -0
  45. cortexshift/domain/errors.py +277 -0
  46. cortexshift/domain/git.py +102 -0
  47. cortexshift/domain/handoff.py +241 -0
  48. cortexshift/domain/identifiers.py +27 -0
  49. cortexshift/domain/launch.py +58 -0
  50. cortexshift/domain/mcp_binding.py +81 -0
  51. cortexshift/domain/native_session.py +19 -0
  52. cortexshift/domain/project.py +37 -0
  53. cortexshift/domain/provider.py +67 -0
  54. cortexshift/domain/session.py +92 -0
  55. cortexshift/domain/status.py +40 -0
  56. cortexshift/domain/task.py +191 -0
  57. cortexshift/mcp/__init__.py +38 -0
  58. cortexshift/mcp/context.py +165 -0
  59. cortexshift/mcp/facade.py +513 -0
  60. cortexshift/mcp/models.py +178 -0
  61. cortexshift/mcp/resources.py +45 -0
  62. cortexshift/mcp/server.py +52 -0
  63. cortexshift/mcp/tools.py +176 -0
  64. cortexshift/ports/__init__.py +39 -0
  65. cortexshift/ports/checkpoint_store.py +45 -0
  66. cortexshift/ports/command_runner.py +56 -0
  67. cortexshift/ports/discovery.py +41 -0
  68. cortexshift/ports/handoff_delivery.py +91 -0
  69. cortexshift/ports/handoff_store.py +43 -0
  70. cortexshift/ports/headless_runner.py +58 -0
  71. cortexshift/ports/native_session.py +20 -0
  72. cortexshift/ports/process_runner.py +31 -0
  73. cortexshift/ports/provider.py +152 -0
  74. cortexshift/ports/repository.py +44 -0
  75. cortexshift/ports/session_store.py +27 -0
  76. cortexshift/ports/state_store.py +55 -0
  77. cortexshift/ports/workspace_lease.py +39 -0
  78. cortexshift/tui/__init__.py +24 -0
  79. cortexshift/tui/actions.py +58 -0
  80. cortexshift/tui/app.py +1051 -0
  81. cortexshift/tui/coordinator.py +173 -0
  82. cortexshift/tui/cortexshift.tcss +258 -0
  83. cortexshift/tui/facade.py +614 -0
  84. cortexshift/tui/modals.py +594 -0
  85. cortexshift/tui/models.py +503 -0
  86. cortexshift/tui/screens/__init__.py +81 -0
  87. cortexshift/tui/screens/checkpoints.py +188 -0
  88. cortexshift/tui/screens/handoffs.py +180 -0
  89. cortexshift/tui/screens/help.py +117 -0
  90. cortexshift/tui/screens/overview.py +200 -0
  91. cortexshift/tui/screens/providers.py +169 -0
  92. cortexshift/tui/screens/repository.py +143 -0
  93. cortexshift/tui/screens/sessions.py +146 -0
  94. cortexshift/tui/screens/task.py +174 -0
  95. cortexshift/tui/widgets.py +209 -0
  96. cortexshift-0.1.0.dist-info/METADATA +202 -0
  97. cortexshift-0.1.0.dist-info/RECORD +100 -0
  98. cortexshift-0.1.0.dist-info/WHEEL +4 -0
  99. cortexshift-0.1.0.dist-info/entry_points.txt +2 -0
  100. cortexshift-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,375 @@
1
+ """Claude Code native CLI probe and discovery adapter."""
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ import sys
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+ from typing import Any
10
+ from uuid import uuid4
11
+
12
+ from cortexshift.domain.doctor import AuthenticationStatus, ProviderDiagnostic
13
+ from cortexshift.domain.errors import NativeResumeError
14
+ from cortexshift.domain.launch import LaunchSpecification
15
+ from cortexshift.domain.mcp_binding import McpSessionBinding
16
+ from cortexshift.domain.native_session import NativeSessionCapabilities, valid_native_id
17
+ from cortexshift.domain.provider import PROVIDER_CLAUDE, ProviderCapabilities, ProviderId
18
+ from cortexshift.ports.command_runner import CommandRunner
19
+ from cortexshift.ports.discovery import ProviderProbe
20
+ from cortexshift.ports.handoff_delivery import (
21
+ HandoffDeliveryPreparation,
22
+ HandoffDeliveryStrategy,
23
+ ProviderHandoffAdapter,
24
+ )
25
+ from cortexshift.ports.provider import ProviderRuntimeAdapter
26
+
27
+ _VERSION_RE = re.compile(r"(\d+\.\d+(?:\.\d+)?(?:[-.][a-zA-Z0-9]+)?)")
28
+
29
+
30
+ def _extract_version(text: str) -> str | None:
31
+ """Extract a clean semver-like version string from CLI output."""
32
+ match = _VERSION_RE.search(text)
33
+ if match:
34
+ return match.group(1)
35
+ first_line = text.splitlines()[0].strip() if text else ""
36
+ return first_line[:32] if first_line and len(first_line) <= 32 else None
37
+
38
+
39
+ def _classify_claude_auth(stdout: str, stderr: str, exit_code: int) -> AuthenticationStatus:
40
+ """Classify Claude auth status conservatively without leaking sensitive output."""
41
+ combined = f"{stdout}\n{stderr}".lower()
42
+
43
+ # Try parsing stdout as JSON if formatted as such
44
+ if stdout.strip().startswith("{"):
45
+ try:
46
+ data = json.loads(stdout)
47
+ if isinstance(data, dict):
48
+ if data.get("loggedIn") is True or data.get("status") == "authenticated":
49
+ return AuthenticationStatus.AUTHENTICATED
50
+ if data.get("loggedIn") is False or data.get("status") == "unauthenticated":
51
+ return AuthenticationStatus.NOT_AUTHENTICATED
52
+ except json.JSONDecodeError:
53
+ pass
54
+
55
+ # Check for definitive unauthenticated markers
56
+ if (
57
+ "not logged in" in combined
58
+ or "logged out" in combined
59
+ or "login required" in combined
60
+ or "run claude auth login" in combined
61
+ ):
62
+ return AuthenticationStatus.NOT_AUTHENTICATED
63
+
64
+ # Check for definitive authenticated markers
65
+ if exit_code == 0 and ("logged in" in combined or "authenticated" in combined):
66
+ return AuthenticationStatus.AUTHENTICATED
67
+
68
+ # Exit code 0 without explicit errors often indicates valid auth for status subcommands
69
+ if exit_code == 0 and not combined.strip():
70
+ return AuthenticationStatus.AUTHENTICATED
71
+
72
+ if exit_code != 0 and ("unauthorized" in combined or "not authenticated" in combined):
73
+ return AuthenticationStatus.NOT_AUTHENTICATED
74
+
75
+ return AuthenticationStatus.UNKNOWN
76
+
77
+
78
+ class ClaudeProviderProbe(ProviderProbe):
79
+ """Probe for detecting and diagnosing the Claude Code native CLI."""
80
+
81
+ def __init__(
82
+ self,
83
+ command_runner: CommandRunner,
84
+ which_fn: Callable[[str], str | None] = shutil.which,
85
+ ) -> None:
86
+ self._runner = command_runner
87
+ self._which = which_fn
88
+
89
+ @property
90
+ def provider_id(self) -> ProviderId:
91
+ return PROVIDER_CLAUDE
92
+
93
+ @property
94
+ def display_name(self) -> str:
95
+ return "Claude Code"
96
+
97
+ @property
98
+ def executable(self) -> str:
99
+ return "claude"
100
+
101
+ def get_capabilities(self) -> ProviderCapabilities:
102
+ return ProviderCapabilities(
103
+ provider_id=self.provider_id,
104
+ display_name=self.display_name,
105
+ supports_interactive=True,
106
+ supports_headless=True,
107
+ supports_native_resume=True,
108
+ supports_structured_output=True,
109
+ supports_mcp=True,
110
+ supports_usage_metrics=True,
111
+ )
112
+
113
+ def probe(self) -> ProviderDiagnostic:
114
+ capabilities = self.get_capabilities()
115
+ resolved_path = self._which(self.executable)
116
+
117
+ if not resolved_path:
118
+ return ProviderDiagnostic(
119
+ provider_id=self.provider_id,
120
+ display_name=self.display_name,
121
+ executable=self.executable,
122
+ installed=False,
123
+ resolved_path=None,
124
+ version=None,
125
+ authentication_status=AuthenticationStatus.UNKNOWN,
126
+ capabilities=capabilities,
127
+ diagnostics=["Not found in PATH"],
128
+ )
129
+
130
+ # 1. Version probe
131
+ version_result = self._runner.run([resolved_path, "--version"], timeout=5.0)
132
+ version: str | None = None
133
+ diagnostics: list[str] = []
134
+
135
+ if version_result.success:
136
+ version = _extract_version(version_result.stdout)
137
+ if not version:
138
+ diagnostics.append("Version output could not be parsed")
139
+ else:
140
+ if version_result.timed_out:
141
+ diagnostics.append("Version probe timed out")
142
+ else:
143
+ diagnostics.append("Version probe failed")
144
+
145
+ # 2. Authentication probe: passive `claude auth status`
146
+ auth_result = self._runner.run([resolved_path, "auth", "status"], timeout=5.0)
147
+ auth_status = _classify_claude_auth(
148
+ stdout=auth_result.stdout,
149
+ stderr=auth_result.stderr,
150
+ exit_code=auth_result.exit_code,
151
+ )
152
+
153
+ if auth_result.timed_out:
154
+ diagnostics.append("Authentication probe timed out")
155
+ elif auth_status == AuthenticationStatus.UNKNOWN and not auth_result.success:
156
+ diagnostics.append("Authentication probe failed")
157
+
158
+ return ProviderDiagnostic(
159
+ provider_id=self.provider_id,
160
+ display_name=self.display_name,
161
+ executable=self.executable,
162
+ installed=True,
163
+ resolved_path=resolved_path,
164
+ version=version,
165
+ authentication_status=auth_status,
166
+ capabilities=capabilities,
167
+ diagnostics=diagnostics,
168
+ )
169
+
170
+
171
+ MCP_CONFIG_FLAG = "--mcp-config"
172
+
173
+
174
+ def build_claude_mcp_config(
175
+ python_executable: str | None = None,
176
+ binding: McpSessionBinding | None = None,
177
+ ) -> str:
178
+ """Build the inline JSON string for Claude Code --mcp-config.
179
+
180
+ When a managed binding is supplied it is declared as the server's own environment, so
181
+ the MCP server is bound to the CortexShift session regardless of how much of Claude
182
+ Code's own environment reaches the server process.
183
+ """
184
+ exe = python_executable or sys.executable
185
+ server: dict[str, Any] = {
186
+ "command": exe,
187
+ "args": ["-m", "cortexshift", "mcp", "serve"],
188
+ }
189
+ if binding is not None:
190
+ server["env"] = binding.to_env()
191
+ return json.dumps({"mcpServers": {"cortexshift": server}})
192
+
193
+
194
+ class ClaudeRuntimeAdapter(ProviderRuntimeAdapter):
195
+ """Runtime adapter for launching Claude Code interactive sessions."""
196
+
197
+ def __init__(self, python_executable: str | None = None) -> None:
198
+ self._python_executable = python_executable
199
+
200
+ @property
201
+ def provider_id(self) -> ProviderId:
202
+ return PROVIDER_CLAUDE
203
+
204
+ @property
205
+ def display_name(self) -> str:
206
+ return "Claude Code"
207
+
208
+ @property
209
+ def executable(self) -> str:
210
+ return "claude"
211
+
212
+ def get_capabilities(self) -> ProviderCapabilities:
213
+ return ProviderCapabilities(
214
+ provider_id=self.provider_id,
215
+ display_name=self.display_name,
216
+ supports_interactive=True,
217
+ supports_headless=True,
218
+ supports_native_resume=True,
219
+ supports_structured_output=True,
220
+ supports_mcp=True,
221
+ supports_usage_metrics=True,
222
+ )
223
+
224
+ def get_native_capabilities(self) -> NativeSessionCapabilities:
225
+ return NativeSessionCapabilities(
226
+ supports_exact_resume=True,
227
+ can_allocate_native_id_before_launch=True,
228
+ can_resume_with_followup_context=True,
229
+ supports_managed_new_session=True,
230
+ )
231
+
232
+ def build_exact_resume(
233
+ self, project_root: Path, executable_path: str, native_session_id: str
234
+ ) -> LaunchSpecification:
235
+ if not valid_native_id(native_session_id):
236
+ raise NativeResumeError("Invalid native session identifier.")
237
+ mcp_config = build_claude_mcp_config(self._python_executable)
238
+ return LaunchSpecification(
239
+ provider_id=self.provider_id,
240
+ executable=executable_path,
241
+ cwd=project_root,
242
+ argv=[
243
+ executable_path,
244
+ MCP_CONFIG_FLAG,
245
+ mcp_config,
246
+ "--resume",
247
+ native_session_id,
248
+ ],
249
+ native_session_id=native_session_id,
250
+ )
251
+
252
+ def bind_managed_mcp(
253
+ self,
254
+ launch_spec: LaunchSpecification,
255
+ binding: McpSessionBinding,
256
+ ) -> LaunchSpecification:
257
+ """Restate the managed binding inside the inline --mcp-config payload."""
258
+ if launch_spec.provider_id != self.provider_id:
259
+ return launch_spec
260
+
261
+ argv = list(launch_spec.argv)
262
+ # A supplied prompt is arbitrary text in the same argv, so it is never scanned:
263
+ # a prompt that happens to read like a flag must not be mistaken for one.
264
+ options = argv[:-1] if launch_spec.prompt_supplied else argv
265
+ if MCP_CONFIG_FLAG not in options:
266
+ return launch_spec
267
+ index = options.index(MCP_CONFIG_FLAG) + 1
268
+ if index >= len(options):
269
+ return launch_spec
270
+
271
+ argv[index] = build_claude_mcp_config(self._python_executable, binding=binding)
272
+ return launch_spec.model_copy(update={"argv": argv})
273
+
274
+ def build_launch_spec(
275
+ self,
276
+ project_root: Path,
277
+ executable_path: str,
278
+ prompt: str | None = None,
279
+ ) -> LaunchSpecification:
280
+ """Build argument vector for native Claude launch."""
281
+ native_id = str(uuid4())
282
+ mcp_config = build_claude_mcp_config(self._python_executable)
283
+ argv = [
284
+ executable_path,
285
+ MCP_CONFIG_FLAG,
286
+ mcp_config,
287
+ "--session-id",
288
+ native_id,
289
+ ]
290
+ prompt_supplied = False
291
+ if prompt is not None and prompt.strip():
292
+ argv.append(prompt)
293
+ prompt_supplied = True
294
+
295
+ return LaunchSpecification(
296
+ provider_id=self.provider_id,
297
+ executable=executable_path,
298
+ cwd=project_root,
299
+ argv=argv,
300
+ native_session_id=native_id,
301
+ interactive=True,
302
+ initial_prompt_supported=True,
303
+ prompt_supplied=prompt_supplied,
304
+ )
305
+
306
+
307
+ class ClaudeHandoffAdapter(ProviderHandoffAdapter):
308
+ """Delivers canonical handoff context to Claude Code's native interactive CLI.
309
+
310
+ Claude Code's documented interactive launch accepts an initial prompt as a trailing
311
+ positional argument, so the canonical context is delivered directly as a single
312
+ argv element. No extra headless model turn is required, no transcript is imported,
313
+ and model, permission mode, and sandbox settings remain the user's own.
314
+ """
315
+
316
+ def __init__(self, runtime_adapter: ClaudeRuntimeAdapter | None = None) -> None:
317
+ self._runtime = runtime_adapter or ClaudeRuntimeAdapter()
318
+
319
+ @property
320
+ def provider_id(self) -> ProviderId:
321
+ return PROVIDER_CLAUDE
322
+
323
+ @property
324
+ def display_name(self) -> str:
325
+ return self._runtime.display_name
326
+
327
+ @property
328
+ def executable(self) -> str:
329
+ return self._runtime.executable
330
+
331
+ @property
332
+ def delivery_strategy(self) -> HandoffDeliveryStrategy:
333
+ return HandoffDeliveryStrategy.DIRECT_INITIAL_PROMPT
334
+
335
+ @property
336
+ def bootstrap_model_turn_required(self) -> bool:
337
+ return False
338
+
339
+ def bind_managed_mcp(
340
+ self,
341
+ launch_spec: LaunchSpecification,
342
+ binding: McpSessionBinding,
343
+ ) -> LaunchSpecification:
344
+ """Delegate managed MCP binding to the runtime adapter that built the argv."""
345
+ return self._runtime.bind_managed_mcp(launch_spec, binding)
346
+
347
+ def prepare_delivery(
348
+ self,
349
+ executable_path: str,
350
+ project_root: Path,
351
+ rendered_context: str,
352
+ native_session_id: str | None = None,
353
+ ) -> HandoffDeliveryPreparation:
354
+ """Build the native interactive launch carrying the handoff as one argument."""
355
+ if native_session_id is not None:
356
+ launch_spec = self._runtime.build_exact_resume(
357
+ project_root, executable_path, native_session_id
358
+ )
359
+ launch_spec = launch_spec.model_copy(
360
+ update={
361
+ "argv": [*launch_spec.argv, rendered_context],
362
+ "prompt_supplied": True,
363
+ }
364
+ )
365
+ else:
366
+ launch_spec = self._runtime.build_launch_spec(
367
+ project_root=project_root,
368
+ executable_path=executable_path,
369
+ prompt=rendered_context,
370
+ )
371
+ return HandoffDeliveryPreparation(
372
+ launch_spec=launch_spec,
373
+ native_session_id=launch_spec.native_session_id,
374
+ bootstrap_performed=False,
375
+ )