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,434 @@
1
+ """OpenAI Codex 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
+
10
+ from cortexshift.adapters.headless_runner import SubprocessHeadlessProviderRunner
11
+ from cortexshift.domain.doctor import AuthenticationStatus, ProviderDiagnostic
12
+ from cortexshift.domain.errors import HandoffDeliveryError, NativeResumeError
13
+ from cortexshift.domain.launch import LaunchSpecification
14
+ from cortexshift.domain.mcp_binding import McpSessionBinding
15
+ from cortexshift.domain.native_session import NativeSessionCapabilities, valid_native_id
16
+ from cortexshift.domain.provider import PROVIDER_CODEX, ProviderCapabilities, ProviderId
17
+ from cortexshift.ports.command_runner import CommandRunner
18
+ from cortexshift.ports.discovery import ProviderProbe
19
+ from cortexshift.ports.handoff_delivery import (
20
+ HandoffDeliveryPreparation,
21
+ HandoffDeliveryStrategy,
22
+ ProviderHandoffAdapter,
23
+ )
24
+ from cortexshift.ports.headless_runner import (
25
+ DEFAULT_HEADLESS_TIMEOUT_SECONDS,
26
+ HeadlessProviderRunner,
27
+ )
28
+ from cortexshift.ports.provider import ProviderRuntimeAdapter
29
+
30
+ _VERSION_RE = re.compile(r"(\d+\.\d+(?:\.\d+)?(?:[-.][a-zA-Z0-9]+)?)")
31
+
32
+
33
+ def _extract_version(text: str) -> str | None:
34
+ """Extract a clean semver-like version string from CLI output."""
35
+ match = _VERSION_RE.search(text)
36
+ if match:
37
+ return match.group(1)
38
+ first_line = text.splitlines()[0].strip() if text else ""
39
+ return first_line[:32] if first_line and len(first_line) <= 32 else None
40
+
41
+
42
+ def _classify_codex_auth(stdout: str, stderr: str, exit_code: int) -> AuthenticationStatus:
43
+ """Classify Codex auth status conservatively without leaking sensitive output."""
44
+ combined = f"{stdout}\n{stderr}".lower()
45
+
46
+ # Try parsing JSON output
47
+ if stdout.strip().startswith("{"):
48
+ try:
49
+ data = json.loads(stdout)
50
+ if isinstance(data, dict):
51
+ if data.get("authenticated") is True or data.get("status") == "logged_in":
52
+ return AuthenticationStatus.AUTHENTICATED
53
+ if data.get("authenticated") is False or data.get("status") == "logged_out":
54
+ return AuthenticationStatus.NOT_AUTHENTICATED
55
+ except json.JSONDecodeError:
56
+ pass
57
+
58
+ # Check for unauthenticated markers
59
+ if (
60
+ "not logged in" in combined
61
+ or "logged out" in combined
62
+ or "no active session" in combined
63
+ or "login required" in combined
64
+ or "run codex login" in combined
65
+ ):
66
+ return AuthenticationStatus.NOT_AUTHENTICATED
67
+
68
+ # Check for authenticated markers
69
+ if exit_code == 0 and ("logged in" in combined or "authenticated" in combined):
70
+ return AuthenticationStatus.AUTHENTICATED
71
+
72
+ if exit_code == 0 and not combined.strip():
73
+ return AuthenticationStatus.AUTHENTICATED
74
+
75
+ if exit_code != 0 and ("unauthorized" in combined or "not authenticated" in combined):
76
+ return AuthenticationStatus.NOT_AUTHENTICATED
77
+
78
+ return AuthenticationStatus.UNKNOWN
79
+
80
+
81
+ class CodexProviderProbe(ProviderProbe):
82
+ """Probe for detecting and diagnosing the Codex native CLI."""
83
+
84
+ def __init__(
85
+ self,
86
+ command_runner: CommandRunner,
87
+ which_fn: Callable[[str], str | None] = shutil.which,
88
+ ) -> None:
89
+ self._runner = command_runner
90
+ self._which = which_fn
91
+
92
+ @property
93
+ def provider_id(self) -> ProviderId:
94
+ return PROVIDER_CODEX
95
+
96
+ @property
97
+ def display_name(self) -> str:
98
+ return "Codex"
99
+
100
+ @property
101
+ def executable(self) -> str:
102
+ return "codex"
103
+
104
+ def get_capabilities(self) -> ProviderCapabilities:
105
+ return ProviderCapabilities(
106
+ provider_id=self.provider_id,
107
+ display_name=self.display_name,
108
+ supports_interactive=True,
109
+ supports_headless=True,
110
+ supports_native_resume=True,
111
+ supports_structured_output=True,
112
+ supports_mcp=True,
113
+ supports_usage_metrics=True,
114
+ )
115
+
116
+ def probe(self) -> ProviderDiagnostic:
117
+ capabilities = self.get_capabilities()
118
+ resolved_path = self._which(self.executable)
119
+
120
+ if not resolved_path:
121
+ return ProviderDiagnostic(
122
+ provider_id=self.provider_id,
123
+ display_name=self.display_name,
124
+ executable=self.executable,
125
+ installed=False,
126
+ resolved_path=None,
127
+ version=None,
128
+ authentication_status=AuthenticationStatus.UNKNOWN,
129
+ capabilities=capabilities,
130
+ diagnostics=["Not found in PATH"],
131
+ )
132
+
133
+ # 1. Version probe
134
+ version_result = self._runner.run([resolved_path, "--version"], timeout=5.0)
135
+ version: str | None = None
136
+ diagnostics: list[str] = []
137
+
138
+ if version_result.success:
139
+ version = _extract_version(version_result.stdout)
140
+ if not version:
141
+ diagnostics.append("Version output could not be parsed")
142
+ else:
143
+ if version_result.timed_out:
144
+ diagnostics.append("Version probe timed out")
145
+ else:
146
+ diagnostics.append("Version probe failed")
147
+
148
+ # 2. Authentication probe: passive `codex login status`
149
+ auth_result = self._runner.run([resolved_path, "login", "status"], timeout=5.0)
150
+ auth_status = _classify_codex_auth(
151
+ stdout=auth_result.stdout,
152
+ stderr=auth_result.stderr,
153
+ exit_code=auth_result.exit_code,
154
+ )
155
+
156
+ if auth_result.timed_out:
157
+ diagnostics.append("Authentication probe timed out")
158
+ elif auth_status == AuthenticationStatus.UNKNOWN and not auth_result.success:
159
+ diagnostics.append("Authentication probe failed")
160
+
161
+ return ProviderDiagnostic(
162
+ provider_id=self.provider_id,
163
+ display_name=self.display_name,
164
+ executable=self.executable,
165
+ installed=True,
166
+ resolved_path=resolved_path,
167
+ version=version,
168
+ authentication_status=auth_status,
169
+ capabilities=capabilities,
170
+ diagnostics=diagnostics,
171
+ )
172
+
173
+
174
+ MCP_SERVER_KEY = "mcp_servers.cortexshift"
175
+
176
+
177
+ def build_codex_mcp_args(
178
+ python_executable: str | None = None,
179
+ binding: McpSessionBinding | None = None,
180
+ ) -> list[str]:
181
+ """Build the argument list of -c overrides for Codex MCP configuration.
182
+
183
+ Codex starts MCP servers with a sanitized environment plus the server's own declared
184
+ `env` table, so a managed binding must be stated here to reach the server at all.
185
+ Values are emitted as JSON, which Codex parses as TOML basic strings; this keeps
186
+ Windows paths, quotes, and non-ASCII characters intact.
187
+ """
188
+ exe = python_executable or sys.executable
189
+ args = [
190
+ "-c",
191
+ f"{MCP_SERVER_KEY}.command={json.dumps(exe, ensure_ascii=False)}",
192
+ "-c",
193
+ f'{MCP_SERVER_KEY}.args=["-m", "cortexshift", "mcp", "serve"]',
194
+ "-c",
195
+ f"{MCP_SERVER_KEY}.required=true",
196
+ ]
197
+ if binding is not None:
198
+ for name, value in binding.to_env().items():
199
+ args.extend(
200
+ ["-c", f"{MCP_SERVER_KEY}.env.{name}={json.dumps(value, ensure_ascii=False)}"]
201
+ )
202
+ return args
203
+
204
+
205
+ class CodexRuntimeAdapter(ProviderRuntimeAdapter):
206
+ """Runtime adapter for launching OpenAI Codex interactive sessions."""
207
+
208
+ def __init__(self, python_executable: str | None = None) -> None:
209
+ self._python_executable = python_executable
210
+
211
+ @property
212
+ def provider_id(self) -> ProviderId:
213
+ return PROVIDER_CODEX
214
+
215
+ @property
216
+ def display_name(self) -> str:
217
+ return "Codex"
218
+
219
+ @property
220
+ def executable(self) -> str:
221
+ return "codex"
222
+
223
+ def get_capabilities(self) -> ProviderCapabilities:
224
+ return ProviderCapabilities(
225
+ provider_id=self.provider_id,
226
+ display_name=self.display_name,
227
+ supports_interactive=True,
228
+ supports_headless=True,
229
+ supports_native_resume=True,
230
+ supports_structured_output=True,
231
+ supports_mcp=True,
232
+ supports_usage_metrics=True,
233
+ )
234
+
235
+ def get_native_capabilities(self) -> NativeSessionCapabilities:
236
+ return NativeSessionCapabilities(
237
+ supports_exact_resume=True,
238
+ can_capture_native_id_during_bootstrap=True,
239
+ can_resume_with_followup_context=True,
240
+ requires_model_turn_for_handoff_resume=True,
241
+ supports_managed_new_session=True,
242
+ )
243
+
244
+ def build_exact_resume(
245
+ self, project_root: Path, executable_path: str, native_session_id: str
246
+ ) -> LaunchSpecification:
247
+ if not valid_native_id(native_session_id):
248
+ raise NativeResumeError("Invalid native session identifier.")
249
+ mcp_args = build_codex_mcp_args(self._python_executable)
250
+ return LaunchSpecification(
251
+ provider_id=self.provider_id,
252
+ executable=executable_path,
253
+ cwd=project_root,
254
+ argv=[executable_path, *mcp_args, "resume", native_session_id],
255
+ native_session_id=native_session_id,
256
+ )
257
+
258
+ def build_launch_spec(
259
+ self,
260
+ project_root: Path,
261
+ executable_path: str,
262
+ prompt: str | None = None,
263
+ ) -> LaunchSpecification:
264
+ """Build argument vector for native Codex launch."""
265
+ mcp_args = build_codex_mcp_args(self._python_executable)
266
+ argv = [executable_path, *mcp_args]
267
+ prompt_supplied = False
268
+ if prompt is not None and prompt.strip():
269
+ argv.append(prompt)
270
+ prompt_supplied = True
271
+
272
+ return LaunchSpecification(
273
+ provider_id=self.provider_id,
274
+ executable=executable_path,
275
+ cwd=project_root,
276
+ argv=argv,
277
+ interactive=True,
278
+ initial_prompt_supported=True,
279
+ prompt_supplied=prompt_supplied,
280
+ )
281
+
282
+ def bind_managed_mcp(
283
+ self,
284
+ launch_spec: LaunchSpecification,
285
+ binding: McpSessionBinding,
286
+ ) -> LaunchSpecification:
287
+ """Restate the managed binding in the -c overrides that configure the MCP server."""
288
+ if launch_spec.provider_id != self.provider_id:
289
+ return launch_spec
290
+
291
+ argv = list(launch_spec.argv)
292
+ # A supplied prompt is arbitrary text in the same argv, so it is never scanned:
293
+ # a prompt that happens to read like an override must not be mistaken for one.
294
+ scan_end = len(argv) - 1 if launch_spec.prompt_supplied else len(argv)
295
+
296
+ remaining: list[str] = []
297
+ insert_at: int | None = None
298
+ index = 0
299
+ while index < len(argv):
300
+ is_override = (
301
+ argv[index] == "-c"
302
+ and index + 1 < scan_end
303
+ and argv[index + 1].startswith(f"{MCP_SERVER_KEY}.")
304
+ )
305
+ if is_override:
306
+ if insert_at is None:
307
+ insert_at = len(remaining)
308
+ index += 2
309
+ continue
310
+ remaining.append(argv[index])
311
+ index += 1
312
+
313
+ if insert_at is None:
314
+ return launch_spec
315
+
316
+ bound = build_codex_mcp_args(self._python_executable, binding=binding)
317
+ remaining[insert_at:insert_at] = bound
318
+ return launch_spec.model_copy(update={"argv": remaining})
319
+
320
+
321
+ CODEX_BOOTSTRAP_PREFIX = """This is a CortexShift handoff bootstrap.
322
+ Analyze and ingest the supplied context. Remain read-only.
323
+ Do not modify repository files. Do not run mutating commands.
324
+ The same native Codex session will be resumed immediately in the interactive TUI.
325
+
326
+ """
327
+
328
+
329
+ class CodexHandoffAdapter(ProviderHandoffAdapter):
330
+ """Capture native identity through one read-only JSONL turn, then open the TUI."""
331
+
332
+ def __init__(
333
+ self,
334
+ runtime_adapter: CodexRuntimeAdapter | None = None,
335
+ headless_runner: HeadlessProviderRunner | None = None,
336
+ timeout: float = DEFAULT_HEADLESS_TIMEOUT_SECONDS,
337
+ ) -> None:
338
+ self._runtime = runtime_adapter or CodexRuntimeAdapter()
339
+ self._headless = headless_runner or SubprocessHeadlessProviderRunner()
340
+ self._timeout = timeout
341
+
342
+ @property
343
+ def provider_id(self) -> ProviderId:
344
+ return PROVIDER_CODEX
345
+
346
+ @property
347
+ def display_name(self) -> str:
348
+ return self._runtime.display_name
349
+
350
+ @property
351
+ def executable(self) -> str:
352
+ return self._runtime.executable
353
+
354
+ @property
355
+ def delivery_strategy(self) -> HandoffDeliveryStrategy:
356
+ return HandoffDeliveryStrategy.READ_ONLY_BOOTSTRAP_THEN_RESUME
357
+
358
+ @property
359
+ def bootstrap_model_turn_required(self) -> bool:
360
+ return True
361
+
362
+ def bind_managed_mcp(
363
+ self,
364
+ launch_spec: LaunchSpecification,
365
+ binding: McpSessionBinding,
366
+ ) -> LaunchSpecification:
367
+ """Delegate managed MCP binding to the runtime adapter that built the argv."""
368
+ return self._runtime.bind_managed_mcp(launch_spec, binding)
369
+
370
+ def prepare_delivery(
371
+ self,
372
+ executable_path: str,
373
+ project_root: Path,
374
+ rendered_context: str,
375
+ native_session_id: str | None = None,
376
+ ) -> HandoffDeliveryPreparation:
377
+ if native_session_id is not None and not valid_native_id(native_session_id):
378
+ raise NativeResumeError("Invalid native session identifier.")
379
+ # Sandbox is an exec parent option, JSON is also supported on exec resume.
380
+ argv = [executable_path, "exec", "--sandbox", "read-only"]
381
+ if native_session_id is not None:
382
+ argv.extend(["resume", "--json", native_session_id])
383
+ else:
384
+ argv.append("--json")
385
+ argv.append(CODEX_BOOTSTRAP_PREFIX + rendered_context)
386
+ result = self._headless.run_headless(
387
+ argv, project_root, timeout=self._timeout, env={"CORTEXSHIFT_MCP_READ_ONLY": "1"}
388
+ )
389
+ if result.timed_out:
390
+ raise HandoffDeliveryError("bootstrap_timeout", "Codex handoff bootstrap timed out.")
391
+ if result.not_found:
392
+ raise HandoffDeliveryError("spawn_failed", "Codex handoff bootstrap could not start.")
393
+ if result.exit_code != 0:
394
+ raise HandoffDeliveryError("bootstrap_failed", "Codex handoff bootstrap failed.")
395
+
396
+ captured: str | None = None
397
+ completed = False
398
+ try:
399
+ for line in result.stdout.splitlines():
400
+ if not line.strip():
401
+ continue
402
+ event = json.loads(line)
403
+ if not isinstance(event, dict):
404
+ raise ValueError
405
+ kind = event.get("type")
406
+ if kind in ("error", "turn.failed"):
407
+ raise HandoffDeliveryError("bootstrap_failed", "Codex handoff turn failed.")
408
+ if kind == "thread.started":
409
+ candidate = event.get("thread_id")
410
+ if not isinstance(candidate, str) or not valid_native_id(candidate):
411
+ raise ValueError
412
+ if captured is not None and captured != candidate:
413
+ raise ValueError
414
+ captured = candidate
415
+ if kind == "turn.completed":
416
+ completed = True
417
+ except (ValueError, TypeError):
418
+ raise HandoffDeliveryError(
419
+ "bootstrap_invalid_output", "Codex returned invalid bootstrap metadata."
420
+ ) from None
421
+ if captured is None or not completed:
422
+ raise HandoffDeliveryError(
423
+ "bootstrap_invalid_output",
424
+ "Codex did not confirm a native thread and completed turn.",
425
+ )
426
+ if native_session_id is not None and captured != native_session_id:
427
+ raise HandoffDeliveryError(
428
+ "bootstrap_invalid_output", "Codex returned a different thread; refusing fallback."
429
+ )
430
+ return HandoffDeliveryPreparation(
431
+ launch_spec=self._runtime.build_exact_resume(project_root, executable_path, captured),
432
+ native_session_id=captured,
433
+ bootstrap_performed=True,
434
+ )
@@ -0,0 +1,10 @@
1
+ """SQLite persistence adapter package."""
2
+
3
+ from cortexshift.adapters.sqlite.migrations import CURRENT_SCHEMA_VERSION, run_migrations
4
+ from cortexshift.adapters.sqlite.store import SQLiteStateStore
5
+
6
+ __all__ = [
7
+ "CURRENT_SCHEMA_VERSION",
8
+ "SQLiteStateStore",
9
+ "run_migrations",
10
+ ]