ctxsync 0.8.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.
- ctxsync/__init__.py +0 -0
- ctxsync/chat_sync.py +186 -0
- ctxsync/cli/__init__.py +3 -0
- ctxsync/cli/auth.py +77 -0
- ctxsync/cli/category.py +71 -0
- ctxsync/cli/chat.py +357 -0
- ctxsync/cli/config.py +72 -0
- ctxsync/cli/file.py +29 -0
- ctxsync/cli/main.py +257 -0
- ctxsync/cli/organization.py +98 -0
- ctxsync/cli/project.py +422 -0
- ctxsync/cli/session.py +626 -0
- ctxsync/cli/submodule.py +148 -0
- ctxsync/cli/sync.py +79 -0
- ctxsync/compression.py +302 -0
- ctxsync/configmanager/__init__.py +5 -0
- ctxsync/configmanager/base_config_manager.py +255 -0
- ctxsync/configmanager/file_config_manager.py +362 -0
- ctxsync/configmanager/inmemory_config_manager.py +134 -0
- ctxsync/exceptions.py +22 -0
- ctxsync/provider_factory.py +38 -0
- ctxsync/providers/__init__.py +0 -0
- ctxsync/providers/base_claude_ai.py +537 -0
- ctxsync/providers/base_provider.py +109 -0
- ctxsync/providers/claude_ai.py +192 -0
- ctxsync/session_key_manager.py +129 -0
- ctxsync/syncmanager.py +328 -0
- ctxsync/utils.py +416 -0
- ctxsync-0.8.0.dist-info/METADATA +151 -0
- ctxsync-0.8.0.dist-info/RECORD +34 -0
- ctxsync-0.8.0.dist-info/WHEEL +5 -0
- ctxsync-0.8.0.dist-info/entry_points.txt +2 -0
- ctxsync-0.8.0.dist-info/licenses/LICENSE +21 -0
- ctxsync-0.8.0.dist-info/top_level.txt +1 -0
ctxsync/cli/project.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
from ..provider_factory import get_provider
|
|
7
|
+
from ..utils import handle_errors, validate_and_get_provider
|
|
8
|
+
from ..exceptions import ProviderError, ConfigurationError
|
|
9
|
+
from .file import file
|
|
10
|
+
from .submodule import submodule
|
|
11
|
+
from ..syncmanager import retry_on_403
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def project():
|
|
18
|
+
"""Manage AI projects within the active organization."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@project.command()
|
|
23
|
+
@click.option(
|
|
24
|
+
"--name",
|
|
25
|
+
default=lambda: os.path.basename(os.getcwd()),
|
|
26
|
+
prompt="Enter a title for your project",
|
|
27
|
+
help="The name of the project (defaults to current directory name)",
|
|
28
|
+
show_default="current directory name",
|
|
29
|
+
)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--description",
|
|
32
|
+
default="Project created with ctxsync",
|
|
33
|
+
prompt="Enter the project description",
|
|
34
|
+
help="The project description",
|
|
35
|
+
show_default=True,
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--local-path",
|
|
39
|
+
default=lambda: os.getcwd(),
|
|
40
|
+
prompt="Enter the absolute path to your local project directory",
|
|
41
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
|
|
42
|
+
help="The local path for the project (defaults to current working directory)",
|
|
43
|
+
show_default="current working directory",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--new",
|
|
47
|
+
is_flag=True,
|
|
48
|
+
help="Create a new remote project on Claude.ai",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--provider",
|
|
52
|
+
type=click.Choice(["claude.ai"], case_sensitive=False),
|
|
53
|
+
default="claude.ai",
|
|
54
|
+
help="The provider to use for this project",
|
|
55
|
+
)
|
|
56
|
+
@click.pass_context
|
|
57
|
+
@handle_errors
|
|
58
|
+
def init(ctx, name, description, local_path, new, provider):
|
|
59
|
+
"""Initialize a new project configuration.
|
|
60
|
+
|
|
61
|
+
If --new is specified, also creates a remote project on Claude.ai.
|
|
62
|
+
Otherwise, only creates the local configuration. Use 'ctxsync organization set'
|
|
63
|
+
and 'ctxsync project set' to link to an existing remote project."""
|
|
64
|
+
|
|
65
|
+
config = ctx.obj
|
|
66
|
+
|
|
67
|
+
# Create .ctxsync directory and save initial config
|
|
68
|
+
ctxsync_dir = os.path.join(local_path, ".ctxsync")
|
|
69
|
+
os.makedirs(ctxsync_dir, exist_ok=True)
|
|
70
|
+
|
|
71
|
+
# Set basic configuration
|
|
72
|
+
config.set("active_provider", provider, local=True)
|
|
73
|
+
config.set("local_path", local_path, local=True)
|
|
74
|
+
|
|
75
|
+
if new:
|
|
76
|
+
# Create remote project if --new flag is specified
|
|
77
|
+
provider_instance = get_provider(config, provider)
|
|
78
|
+
|
|
79
|
+
# Get organization
|
|
80
|
+
organizations = provider_instance.get_organizations()
|
|
81
|
+
if not organizations:
|
|
82
|
+
raise ConfigurationError(
|
|
83
|
+
"No organizations with required capabilities found."
|
|
84
|
+
)
|
|
85
|
+
organization = organizations[0]["id"]
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
new_project = provider_instance.create_project(
|
|
89
|
+
organization, name, description
|
|
90
|
+
)
|
|
91
|
+
click.echo(
|
|
92
|
+
f"Project '{new_project['name']}' (uuid: {new_project['uuid']}) has been created successfully."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Update configuration with remote details
|
|
96
|
+
config.set("active_organization_id", organization, local=True)
|
|
97
|
+
config.set("active_project_id", new_project["uuid"], local=True)
|
|
98
|
+
config.set("active_project_name", new_project["name"], local=True)
|
|
99
|
+
|
|
100
|
+
click.echo("\nProject created:")
|
|
101
|
+
click.echo(f" - Project location: {local_path}")
|
|
102
|
+
click.echo(
|
|
103
|
+
f" - Project config location: {os.path.join(ctxsync_dir, 'config.local.json')}"
|
|
104
|
+
)
|
|
105
|
+
click.echo(
|
|
106
|
+
f" - Remote URL: https://claude.ai/project/{new_project['uuid']}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
except (ProviderError, ConfigurationError) as e:
|
|
110
|
+
click.echo(f"Failed to create remote project: {str(e)}")
|
|
111
|
+
raise click.Abort()
|
|
112
|
+
else:
|
|
113
|
+
config._save_local_config()
|
|
114
|
+
click.echo("\nLocal project configuration created:")
|
|
115
|
+
click.echo(f" - Project location: {local_path}")
|
|
116
|
+
click.echo(
|
|
117
|
+
f" - Project config location: {os.path.join(ctxsync_dir, 'config.local.json')}"
|
|
118
|
+
)
|
|
119
|
+
click.echo("\nTo link to a remote project:")
|
|
120
|
+
click.echo("1. Run 'ctxsync organization set' to select an organization")
|
|
121
|
+
click.echo("2. Run 'ctxsync project set' to select an existing project")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@project.command()
|
|
125
|
+
@click.pass_context
|
|
126
|
+
def create(ctx, **kwargs):
|
|
127
|
+
"""Create a new project (alias for 'init --new')."""
|
|
128
|
+
# Forward to init command with --new flag
|
|
129
|
+
ctx.forward(init, new=True)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@project.command()
|
|
133
|
+
@click.option(
|
|
134
|
+
"-a",
|
|
135
|
+
"--all",
|
|
136
|
+
"archive_all",
|
|
137
|
+
is_flag=True,
|
|
138
|
+
help="Archive all active projects",
|
|
139
|
+
)
|
|
140
|
+
@click.option(
|
|
141
|
+
"-y",
|
|
142
|
+
"--yes",
|
|
143
|
+
is_flag=True,
|
|
144
|
+
help="Skip confirmation prompt",
|
|
145
|
+
)
|
|
146
|
+
@click.pass_obj
|
|
147
|
+
@handle_errors
|
|
148
|
+
def archive(config, archive_all, yes):
|
|
149
|
+
"""Archive existing projects."""
|
|
150
|
+
provider = validate_and_get_provider(config)
|
|
151
|
+
active_organization_id = config.get("active_organization_id")
|
|
152
|
+
projects = provider.get_projects(active_organization_id, include_archived=False)
|
|
153
|
+
|
|
154
|
+
if not projects:
|
|
155
|
+
click.echo("No active projects found.")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
if archive_all:
|
|
159
|
+
if not yes:
|
|
160
|
+
click.echo("The following projects will be archived:")
|
|
161
|
+
for project in projects:
|
|
162
|
+
click.echo(f" - {project['name']} (ID: {project['id']})")
|
|
163
|
+
if not click.confirm("Are you sure you want to archive all projects?"):
|
|
164
|
+
click.echo("Operation cancelled.")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
with click.progressbar(
|
|
168
|
+
projects,
|
|
169
|
+
label="Archiving projects",
|
|
170
|
+
item_show_func=lambda p: p["name"] if p else "",
|
|
171
|
+
) as bar:
|
|
172
|
+
for project in bar:
|
|
173
|
+
try:
|
|
174
|
+
provider.archive_project(active_organization_id, project["id"])
|
|
175
|
+
except Exception as e:
|
|
176
|
+
click.echo(
|
|
177
|
+
f"\nFailed to archive project '{project['name']}': {str(e)}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
click.echo("\nArchive operation completed.")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
single_project_archival(projects, yes, provider, active_organization_id)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _save_project_selection(config, selected_project):
|
|
187
|
+
"""Save project selection to config and display confirmation."""
|
|
188
|
+
config.set("active_project_id", selected_project["id"], local=True)
|
|
189
|
+
config.set("active_project_name", selected_project["name"], local=True)
|
|
190
|
+
click.echo(
|
|
191
|
+
f"Selected project: {selected_project['name']} (ID: {selected_project['id']})"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Create .ctxsync directory in the current working directory if it doesn't exist
|
|
195
|
+
os.makedirs(".ctxsync", exist_ok=True)
|
|
196
|
+
ctxsync_dir = os.path.abspath(".ctxsync")
|
|
197
|
+
config_file_path = os.path.join(ctxsync_dir, "config.local.json")
|
|
198
|
+
config._save_local_config()
|
|
199
|
+
|
|
200
|
+
click.echo("\nProject created:")
|
|
201
|
+
click.echo(f" - Project location: {os.getcwd()}")
|
|
202
|
+
click.echo(f" - Project config location: {config_file_path}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def single_project_archival(projects, yes, provider, active_organization_id):
|
|
206
|
+
click.echo("Available projects to archive:")
|
|
207
|
+
for idx, project in enumerate(projects, 1):
|
|
208
|
+
click.echo(f" {idx}. {project['name']} (ID: {project['id']})")
|
|
209
|
+
|
|
210
|
+
selection = click.prompt("Enter the number of the project to archive", type=int)
|
|
211
|
+
if 1 <= selection <= len(projects):
|
|
212
|
+
selected_project = projects[selection - 1]
|
|
213
|
+
if yes or click.confirm(
|
|
214
|
+
f"Are you sure you want to archive the project '{selected_project['name']}'? "
|
|
215
|
+
f"Archived projects cannot be modified but can still be viewed."
|
|
216
|
+
):
|
|
217
|
+
provider.archive_project(active_organization_id, selected_project["id"])
|
|
218
|
+
click.echo(f"Project '{selected_project['name']}' has been archived.")
|
|
219
|
+
else:
|
|
220
|
+
click.echo("Invalid selection. Please try again.")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@project.command()
|
|
224
|
+
@click.option(
|
|
225
|
+
"-a",
|
|
226
|
+
"--all",
|
|
227
|
+
"show_all",
|
|
228
|
+
is_flag=True,
|
|
229
|
+
help="Include submodule projects in the selection",
|
|
230
|
+
)
|
|
231
|
+
@click.option(
|
|
232
|
+
"--project-id",
|
|
233
|
+
help="UUID of the project to set as active (skips interactive selection)",
|
|
234
|
+
)
|
|
235
|
+
@click.option(
|
|
236
|
+
"--provider",
|
|
237
|
+
type=click.Choice(["claude.ai"]), # Add more providers as they become available
|
|
238
|
+
default="claude.ai",
|
|
239
|
+
help="Specify the provider for repositories without .ctxsync",
|
|
240
|
+
)
|
|
241
|
+
@click.pass_context
|
|
242
|
+
@handle_errors
|
|
243
|
+
def set(ctx, show_all, project_id, provider):
|
|
244
|
+
"""Set the active project for syncing."""
|
|
245
|
+
config = ctx.obj
|
|
246
|
+
|
|
247
|
+
# If provider is not specified, try to get it from the config
|
|
248
|
+
if not provider:
|
|
249
|
+
provider = config.get("active_provider")
|
|
250
|
+
|
|
251
|
+
# If provider is still not available, prompt the user
|
|
252
|
+
if not provider:
|
|
253
|
+
provider = click.prompt(
|
|
254
|
+
"Please specify the provider",
|
|
255
|
+
type=click.Choice(
|
|
256
|
+
["claude.ai"]
|
|
257
|
+
), # Add more providers as they become available
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Update the config with the provider
|
|
261
|
+
config.set("active_provider", provider, local=True)
|
|
262
|
+
|
|
263
|
+
# Now we can get the provider instance
|
|
264
|
+
provider_instance = validate_and_get_provider(config)
|
|
265
|
+
active_organization_id = config.get("active_organization_id")
|
|
266
|
+
active_project_name = config.get("active_project_name")
|
|
267
|
+
projects = provider_instance.get_projects(
|
|
268
|
+
active_organization_id, include_archived=False
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if show_all:
|
|
272
|
+
selectable_projects = projects
|
|
273
|
+
else:
|
|
274
|
+
# Filter out submodule projects
|
|
275
|
+
selectable_projects = [p for p in projects if "-SubModule-" not in p["name"]]
|
|
276
|
+
|
|
277
|
+
if not selectable_projects:
|
|
278
|
+
click.echo("No active projects found.")
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
# Non-interactive mode: if project_id is provided, use it directly
|
|
282
|
+
if project_id:
|
|
283
|
+
selected_project = next(
|
|
284
|
+
(p for p in selectable_projects if p["id"] == project_id), None
|
|
285
|
+
)
|
|
286
|
+
if selected_project:
|
|
287
|
+
_save_project_selection(config, selected_project)
|
|
288
|
+
else:
|
|
289
|
+
click.echo(f"Project with ID {project_id} not found in available projects.")
|
|
290
|
+
if not show_all:
|
|
291
|
+
click.echo("Tip: Use --all flag to include submodule projects.")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
# Interactive mode: display list and prompt for selection
|
|
295
|
+
click.echo("Available projects:")
|
|
296
|
+
for idx, project in enumerate(selectable_projects, 1):
|
|
297
|
+
project_type = (
|
|
298
|
+
"Main Project"
|
|
299
|
+
if not project["name"].startswith(f"{active_project_name}-SubModule-")
|
|
300
|
+
else "Submodule"
|
|
301
|
+
)
|
|
302
|
+
click.echo(f" {idx}. {project['name']} (ID: {project['id']}) - {project_type}")
|
|
303
|
+
|
|
304
|
+
selection = click.prompt(
|
|
305
|
+
"Enter the number of the project to select", type=int, default=1
|
|
306
|
+
)
|
|
307
|
+
if 1 <= selection <= len(selectable_projects):
|
|
308
|
+
selected_project = selectable_projects[selection - 1]
|
|
309
|
+
_save_project_selection(config, selected_project)
|
|
310
|
+
else:
|
|
311
|
+
click.echo("Invalid selection. Please try again.")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@project.command()
|
|
315
|
+
@click.option(
|
|
316
|
+
"-a",
|
|
317
|
+
"--all",
|
|
318
|
+
"show_all",
|
|
319
|
+
is_flag=True,
|
|
320
|
+
help="Include archived projects in the list",
|
|
321
|
+
)
|
|
322
|
+
@click.pass_obj
|
|
323
|
+
@handle_errors
|
|
324
|
+
def ls(config, show_all):
|
|
325
|
+
"""List all projects in the active organization."""
|
|
326
|
+
provider = validate_and_get_provider(config)
|
|
327
|
+
active_organization_id = config.get("active_organization_id")
|
|
328
|
+
projects = provider.get_projects(active_organization_id, include_archived=show_all)
|
|
329
|
+
if not projects:
|
|
330
|
+
click.echo("No projects found.")
|
|
331
|
+
else:
|
|
332
|
+
click.echo("Remote projects:")
|
|
333
|
+
for project in projects:
|
|
334
|
+
status = " (Archived)" if project.get("archived_at") else ""
|
|
335
|
+
click.echo(f" - {project['name']} (ID: {project['id']}){status}")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@project.command()
|
|
339
|
+
@click.option(
|
|
340
|
+
"-a", "--include-archived", is_flag=True, help="Include archived projects"
|
|
341
|
+
)
|
|
342
|
+
@click.option("--all", "truncate_all", is_flag=True, help="Truncate all projects")
|
|
343
|
+
@click.option("-y", "--yes", is_flag=True, help="Skip confirmation prompt")
|
|
344
|
+
@click.pass_obj
|
|
345
|
+
@handle_errors
|
|
346
|
+
def truncate(config, include_archived, truncate_all, yes):
|
|
347
|
+
"""Truncate one or all projects."""
|
|
348
|
+
provider = validate_and_get_provider(config)
|
|
349
|
+
active_organization_id = config.get("active_organization_id")
|
|
350
|
+
|
|
351
|
+
projects = provider.get_projects(
|
|
352
|
+
active_organization_id, include_archived=include_archived
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if not projects:
|
|
356
|
+
click.echo("No projects found.")
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
if truncate_all:
|
|
360
|
+
if not yes:
|
|
361
|
+
click.echo("This will delete ALL files from the following projects:")
|
|
362
|
+
for project in projects:
|
|
363
|
+
status = " (Archived)" if project.get("archived_at") else ""
|
|
364
|
+
click.echo(f" - {project['name']} (ID: {project['id']}){status}")
|
|
365
|
+
if not click.confirm(
|
|
366
|
+
"Are you sure you want to continue? This may take some time."
|
|
367
|
+
):
|
|
368
|
+
click.echo("Operation cancelled.")
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
with tqdm(total=len(projects), desc="Deleting files from projects") as pbar:
|
|
372
|
+
for project in projects:
|
|
373
|
+
delete_files_from_project(
|
|
374
|
+
provider, active_organization_id, project["id"], project["name"]
|
|
375
|
+
)
|
|
376
|
+
pbar.update(1)
|
|
377
|
+
|
|
378
|
+
click.echo("All files have been deleted from all projects.")
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
click.echo("Available projects:")
|
|
382
|
+
for idx, project in enumerate(projects, 1):
|
|
383
|
+
status = " (Archived)" if project.get("archived_at") else ""
|
|
384
|
+
click.echo(f" {idx}. {project['name']} (ID: {project['id']}){status}")
|
|
385
|
+
|
|
386
|
+
selection = click.prompt("Enter the number of the project to truncate", type=int)
|
|
387
|
+
if 1 <= selection <= len(projects):
|
|
388
|
+
selected_project = projects[selection - 1]
|
|
389
|
+
if yes or click.confirm(
|
|
390
|
+
f"Are you sure you want to delete ALL files from project '{selected_project['name']}'?"
|
|
391
|
+
):
|
|
392
|
+
delete_files_from_project(
|
|
393
|
+
provider,
|
|
394
|
+
active_organization_id,
|
|
395
|
+
selected_project["id"],
|
|
396
|
+
selected_project["name"],
|
|
397
|
+
)
|
|
398
|
+
click.echo(
|
|
399
|
+
f"All files have been deleted from project '{selected_project['name']}'."
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
402
|
+
click.echo("Invalid selection. Please try again.")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@retry_on_403()
|
|
406
|
+
def delete_files_from_project(provider, organization_id, project_id, project_name):
|
|
407
|
+
try:
|
|
408
|
+
files = provider.list_files(organization_id, project_id)
|
|
409
|
+
with tqdm(
|
|
410
|
+
total=len(files), desc=f"Deleting files from {project_name}", leave=False
|
|
411
|
+
) as file_pbar:
|
|
412
|
+
for current_file in files:
|
|
413
|
+
provider.delete_file(organization_id, project_id, current_file["uuid"])
|
|
414
|
+
file_pbar.update(1)
|
|
415
|
+
except ProviderError as e:
|
|
416
|
+
click.echo(f"Error deleting files from project {project_name}: {str(e)}")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
project.add_command(submodule)
|
|
420
|
+
project.add_command(file)
|
|
421
|
+
|
|
422
|
+
__all__ = ["project"]
|