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,31 @@
1
+ """Port defining the interface for running interactive provider processes."""
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol, runtime_checkable
5
+
6
+
7
+ @runtime_checkable
8
+ class InteractiveProcessRunner(Protocol):
9
+ """Abstract port for running long-lived interactive provider processes.
10
+
11
+ Unlike diagnostic command runners, interactive runners inherit terminal
12
+ streams (stdin, stdout, stderr) and do not impose short timeouts.
13
+ """
14
+
15
+ def run_interactive(
16
+ self,
17
+ argv: list[str],
18
+ cwd: Path | str,
19
+ env: dict[str, str] | None = None,
20
+ ) -> int:
21
+ """Run a process interactively inheriting the terminal.
22
+
23
+ Args:
24
+ argv: Argument vector to execute without a shell.
25
+ cwd: Canonical working directory for execution.
26
+ env: Optional environment dictionary overlay.
27
+
28
+ Returns:
29
+ The exit code of the terminated process.
30
+ """
31
+ ...
@@ -0,0 +1,152 @@
1
+ """Port defining the interface for AI coding agent provider adapters."""
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ from cortexshift.domain.handoff import HandoffRecord
7
+ from cortexshift.domain.launch import LaunchSpecification
8
+ from cortexshift.domain.mcp_binding import McpSessionBinding
9
+ from cortexshift.domain.provider import ProviderCapabilities, ProviderId
10
+ from cortexshift.domain.task import Task
11
+
12
+
13
+ @runtime_checkable
14
+ class ProviderAdapter(Protocol):
15
+ """Abstract port for orchestrating an AI coding agent CLI.
16
+
17
+ Concrete implementations wrap native provider CLIs (e.g. Claude Code,
18
+ Codex, Antigravity) without polluting the core domain with vendor specifics.
19
+ """
20
+
21
+ @property
22
+ def provider_id(self) -> ProviderId:
23
+ """The stable unique identifier for this provider."""
24
+ ...
25
+
26
+ def probe(self) -> bool:
27
+ """Check whether the native CLI is installed, licensed, and executable."""
28
+ ...
29
+
30
+ def get_capabilities(self) -> ProviderCapabilities:
31
+ """Return the declared operational capabilities of this provider."""
32
+ ...
33
+
34
+ def launch_interactive(
35
+ self,
36
+ task: Task,
37
+ handoff: HandoffRecord | None = None,
38
+ ) -> int:
39
+ """Launch an interactive terminal session with this provider on the given task.
40
+
41
+ Args:
42
+ task: The persistent task being worked on.
43
+ handoff: Advisory handoff payload if transitioning from another agent.
44
+
45
+ Returns:
46
+ Exit code of the process.
47
+ """
48
+ ...
49
+
50
+ def run_headless(
51
+ self,
52
+ task: Task,
53
+ instruction: str,
54
+ handoff: HandoffRecord | None = None,
55
+ ) -> int:
56
+ """Run a non-interactive single-command session if supported.
57
+
58
+ Args:
59
+ task: The persistent task being worked on.
60
+ instruction: Prompt or command to execute.
61
+ handoff: Advisory handoff payload if available.
62
+
63
+ Returns:
64
+ Exit code of the process.
65
+ """
66
+ ...
67
+
68
+ def resume_native_session(
69
+ self,
70
+ task: Task,
71
+ native_session_id: str,
72
+ ) -> int:
73
+ """Resume a previous provider-native session if supported.
74
+
75
+ Args:
76
+ task: The persistent task being worked on.
77
+ native_session_id: Provider's internal thread or session ID.
78
+
79
+ Returns:
80
+ Exit code of the process.
81
+ """
82
+ ...
83
+
84
+
85
+ @runtime_checkable
86
+ class ProviderRuntimeAdapter(Protocol):
87
+ """Port for building provider launch specifications without invoking processes directly."""
88
+
89
+ @property
90
+ def provider_id(self) -> ProviderId:
91
+ """Canonical provider identifier."""
92
+ ...
93
+
94
+ @property
95
+ def display_name(self) -> str:
96
+ """Human-readable display name."""
97
+ ...
98
+
99
+ @property
100
+ def executable(self) -> str:
101
+ """Base name of the provider CLI executable."""
102
+ ...
103
+
104
+ def get_capabilities(self) -> ProviderCapabilities:
105
+ """Return declared operational capabilities of this provider."""
106
+ ...
107
+
108
+ def build_launch_spec(
109
+ self,
110
+ project_root: Path,
111
+ executable_path: str,
112
+ prompt: str | None = None,
113
+ ) -> LaunchSpecification:
114
+ """Build concrete launch specification for this provider.
115
+
116
+ Args:
117
+ project_root: Absolute project root directory to serve as cwd.
118
+ executable_path: Resolved absolute or PATH path to the executable.
119
+ prompt: Optional initial prompt.
120
+
121
+ Returns:
122
+ A validated LaunchSpecification.
123
+
124
+ Raises:
125
+ UnsupportedPromptError: If prompt is provided but not supported for interactive launch.
126
+ """
127
+ ...
128
+
129
+
130
+ @runtime_checkable
131
+ class ManagedMcpBinder(Protocol):
132
+ """Optional port for providers that configure the CortexShift MCP server per launch.
133
+
134
+ A provider CLI spawns the MCP server itself, so CortexShift cannot rely on its own
135
+ process environment reaching that grandchild: Codex, for one, sanitizes it. Providers
136
+ implementing this port restate the trusted binding inside the MCP configuration they
137
+ were launched with. Providers configured out of band, such as Antigravity's workspace
138
+ file, implement nothing and keep their existing behaviour.
139
+ """
140
+
141
+ def bind_managed_mcp(
142
+ self,
143
+ launch_spec: LaunchSpecification,
144
+ binding: McpSessionBinding,
145
+ ) -> LaunchSpecification:
146
+ """Return the launch specification with the managed binding declared to MCP.
147
+
148
+ Implementations must be total: a specification carrying no CortexShift MCP
149
+ configuration, or one belonging to another provider, is returned unchanged rather
150
+ than rejected.
151
+ """
152
+ ...
@@ -0,0 +1,44 @@
1
+ """Port defining repository inspection and snapshot persistence capabilities."""
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ from cortexshift.domain.git import GitSnapshot, RepositoryInspection
7
+
8
+
9
+ @runtime_checkable
10
+ class RepositoryInspector(Protocol):
11
+ """Abstract port for inspecting the repository filesystem and Git status.
12
+
13
+ Extracts empirical repository state without embedding shell or git specifics
14
+ into core domain logic.
15
+ """
16
+
17
+ def inspect(self, project_root: Path | str, project_id: str = "") -> RepositoryInspection:
18
+ """Inspect the repository and return a RepositoryInspection domain model.
19
+
20
+ Args:
21
+ project_root: Absolute path to the CortexShift project root directory.
22
+ project_id: Canonical identifier of the containing Project.
23
+
24
+ Returns:
25
+ A populated RepositoryInspection domain model.
26
+ """
27
+ ...
28
+
29
+
30
+ @runtime_checkable
31
+ class RepositorySnapshotStore(Protocol):
32
+ """Abstract port for persisting and retrieving Git snapshots."""
33
+
34
+ def save_snapshot(self, snapshot: GitSnapshot) -> None:
35
+ """Persist a GitSnapshot."""
36
+ ...
37
+
38
+ def get_snapshot(self, snapshot_id: str) -> GitSnapshot | None:
39
+ """Retrieve a GitSnapshot by its unique identifier."""
40
+ ...
41
+
42
+ def list_snapshots(self, project_id: str, limit: int = 10) -> list[GitSnapshot]:
43
+ """List snapshots for a given project, ordered newest first."""
44
+ ...
@@ -0,0 +1,27 @@
1
+ """Port defining persistence boundaries for CortexShift agent sessions."""
2
+
3
+ from typing import Protocol, runtime_checkable
4
+
5
+ from cortexshift.domain.session import Session
6
+
7
+
8
+ @runtime_checkable
9
+ class SessionStore(Protocol):
10
+ """Abstract port for persisting and querying CortexShift agent execution sessions."""
11
+
12
+ def save_session(self, session: Session) -> None:
13
+ """Persist or update a Session record."""
14
+ ...
15
+
16
+ def get_session(self, session_id: str) -> Session | None:
17
+ """Retrieve a Session by its stable identifier."""
18
+ ...
19
+
20
+ def list_sessions(
21
+ self,
22
+ project_id: str | None = None,
23
+ task_id: str | None = None,
24
+ limit: int | None = 20,
25
+ ) -> list[Session]:
26
+ """List sessions, ordered newest first."""
27
+ ...
@@ -0,0 +1,55 @@
1
+ """Port defining persistence boundaries for CortexShift state."""
2
+
3
+ from typing import Protocol, runtime_checkable
4
+
5
+ from cortexshift.domain.project import Project
6
+ from cortexshift.domain.task import Task
7
+
8
+
9
+ @runtime_checkable
10
+ class StateStore(Protocol):
11
+ """Abstract port for persisting and querying CortexShift entities.
12
+
13
+ Isolates domain and application logic from the underlying storage mechanism
14
+ (e.g., local SQLite, in-memory test store).
15
+ """
16
+
17
+ # Project operations
18
+ def save_project(self, project: Project) -> None:
19
+ """Persist or update a Project."""
20
+ ...
21
+
22
+ def get_project(self, project_id: str) -> Project | None:
23
+ """Retrieve a Project by its stable identifier."""
24
+ ...
25
+
26
+ def get_default_project(self) -> Project | None:
27
+ """Retrieve the canonical project associated with this project-local store."""
28
+ ...
29
+
30
+ # Task operations
31
+ def save_task(self, task: Task) -> None:
32
+ """Persist or update a Task."""
33
+ ...
34
+
35
+ def get_task(self, task_id: str) -> Task | None:
36
+ """Retrieve a Task by its stable identifier."""
37
+ ...
38
+
39
+ def list_tasks(self, project_id: str) -> list[Task]:
40
+ """List all tasks associated with a given project."""
41
+ ...
42
+
43
+ # Runtime / Active Task state
44
+ def get_active_task_id(self, project_id: str) -> str | None:
45
+ """Retrieve the identifier of the active task for the project, if any."""
46
+ ...
47
+
48
+ def set_active_task_id(self, project_id: str, task_id: str | None) -> None:
49
+ """Set or clear the active task identifier for the project."""
50
+ ...
51
+
52
+ # Schema inspection
53
+ def get_schema_version(self) -> int:
54
+ """Retrieve the current applied database schema version."""
55
+ ...
@@ -0,0 +1,39 @@
1
+ """Port defining exclusive workspace leasing to enforce single mutating agent invariant."""
2
+
3
+ from pathlib import Path
4
+ from typing import Protocol, runtime_checkable
5
+
6
+
7
+ @runtime_checkable
8
+ class WorkspaceLease(Protocol):
9
+ """An acquired or acquirable exclusive lease on a CortexShift workspace."""
10
+
11
+ @property
12
+ def lock_path(self) -> Path:
13
+ """The absolute path to the lock file."""
14
+ ...
15
+
16
+ def acquire(self) -> bool:
17
+ """Attempt to acquire the exclusive non-blocking lease.
18
+
19
+ Returns:
20
+ True if the lease was acquired successfully, False if already held.
21
+ """
22
+ ...
23
+
24
+ def release(self) -> None:
25
+ """Release the held exclusive lease."""
26
+ ...
27
+
28
+ def is_locked(self) -> bool:
29
+ """Check whether the workspace is currently locked by any process."""
30
+ ...
31
+
32
+
33
+ @runtime_checkable
34
+ class WorkspaceLeaseManager(Protocol):
35
+ """Factory port for creating workspace lease instances for a project."""
36
+
37
+ def get_lease(self, project_root: Path) -> WorkspaceLease:
38
+ """Return a WorkspaceLease configured for the given project root."""
39
+ ...
@@ -0,0 +1,24 @@
1
+ """CortexShift interactive terminal control center (Textual).
2
+
3
+ The TUI is an adapter over the existing application services — the same services the CLI
4
+ and the MCP server use. It owns presentation only: no SQL, no SQLite handles, no Git
5
+ subprocesses, no provider argv construction, and no duplicated task, recovery, or handoff
6
+ rules.
7
+ """
8
+
9
+ from cortexshift.tui.actions import TuiExitAction, TuiExitRequest
10
+ from cortexshift.tui.app import CortexShiftApp, build_app
11
+ from cortexshift.tui.coordinator import TuiCoordinator, TuiSession
12
+ from cortexshift.tui.facade import TuiFacade
13
+ from cortexshift.tui.screens import TuiSection
14
+
15
+ __all__ = [
16
+ "CortexShiftApp",
17
+ "TuiCoordinator",
18
+ "TuiExitAction",
19
+ "TuiExitRequest",
20
+ "TuiFacade",
21
+ "TuiSection",
22
+ "TuiSession",
23
+ "build_app",
24
+ ]
@@ -0,0 +1,58 @@
1
+ """The structured boundary between the dashboard and native provider launch.
2
+
3
+ CortexShift never embeds a provider's terminal UI. When the operator chooses to run,
4
+ resume, or switch, the Textual application exits and *returns* a `TuiExitRequest`. Only
5
+ after `App.run()` has returned — and Textual has restored the terminal — does the
6
+ coordinator invoke `RunService`, `ResumeService`, or `SwitchService`, handing raw terminal
7
+ ownership to the native provider process.
8
+
9
+ This module deliberately depends on nothing from Textual so the contract can be
10
+ constructed, asserted, and dispatched without a running application.
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+ from enum import StrEnum
15
+
16
+
17
+ class TuiExitAction(StrEnum):
18
+ """A provider action the dashboard defers to the terminal coordinator."""
19
+
20
+ RUN = "run"
21
+ RESUME = "resume"
22
+ SWITCH = "switch"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class TuiExitRequest:
27
+ """A request to launch a native provider after the dashboard has exited.
28
+
29
+ Constructing one launches nothing. It is a value returned out of the Textual
30
+ application, describing what the coordinator should do once the terminal is free.
31
+ """
32
+
33
+ action: TuiExitAction
34
+ provider: str
35
+ selected_session_id: str | None = None
36
+ force_new_session: bool = False
37
+ note: str | None = None
38
+
39
+ def __post_init__(self) -> None:
40
+ provider = self.provider.strip().lower()
41
+ if not provider:
42
+ raise ValueError("A provider identifier is required for a TUI exit request.")
43
+ object.__setattr__(self, "provider", provider)
44
+
45
+ if self.force_new_session and self.selected_session_id is not None:
46
+ raise ValueError("force_new_session and selected_session_id are mutually exclusive.")
47
+ if self.action is TuiExitAction.RUN and self.selected_session_id is not None:
48
+ raise ValueError("A run request never targets an existing session.")
49
+
50
+ @property
51
+ def description(self) -> str:
52
+ """Human-readable summary used in coordinator output and confirmations."""
53
+ verb = {
54
+ TuiExitAction.RUN: "Run",
55
+ TuiExitAction.RESUME: "Resume",
56
+ TuiExitAction.SWITCH: "Switch to",
57
+ }[self.action]
58
+ return f"{verb} {self.provider}"