claudesync 0.2.6__tar.gz → 0.2.8__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.
- {claudesync-0.2.6 → claudesync-0.2.8}/LICENSE +1 -1
- claudesync-0.2.8/PKG-INFO +148 -0
- claudesync-0.2.8/README.md +109 -0
- {claudesync-0.2.6 → claudesync-0.2.8}/pyproject.toml +7 -5
- claudesync-0.2.8/src/claudesync/__init__.py +0 -0
- claudesync-0.2.8/src/claudesync/cli.py +357 -0
- claudesync-0.2.8/src/claudesync/config_manager.py +30 -0
- claudesync-0.2.8/src/claudesync/exceptions.py +9 -0
- claudesync-0.2.8/src/claudesync/provider_factory.py +17 -0
- claudesync-0.2.8/src/claudesync/providers/__init__.py +0 -0
- claudesync-0.2.8/src/claudesync/providers/claude_ai.py +154 -0
- claudesync-0.2.8/src/claudesync/utils.py +57 -0
- claudesync-0.2.8/src/claudesync.egg-info/PKG-INFO +148 -0
- claudesync-0.2.8/src/claudesync.egg-info/SOURCES.txt +20 -0
- claudesync-0.2.8/src/claudesync.egg-info/entry_points.txt +2 -0
- {claudesync-0.2.6 → claudesync-0.2.8}/src/claudesync.egg-info/requires.txt +2 -2
- claudesync-0.2.8/tests/test_cli.py +56 -0
- claudesync-0.2.8/tests/test_utils.py +54 -0
- claudesync-0.2.6/PKG-INFO +0 -85
- claudesync-0.2.6/README.md +0 -68
- claudesync-0.2.6/src/claudesync/__init__.py +0 -7
- claudesync-0.2.6/src/claudesync/api_utils.py +0 -94
- claudesync-0.2.6/src/claudesync/debounce.py +0 -21
- claudesync-0.2.6/src/claudesync/file_handler.py +0 -103
- claudesync-0.2.6/src/claudesync/gitignore_utils.py +0 -31
- claudesync-0.2.6/src/claudesync/main.py +0 -156
- claudesync-0.2.6/src/claudesync/manual_auth.py +0 -77
- claudesync-0.2.6/src/claudesync.egg-info/PKG-INFO +0 -85
- claudesync-0.2.6/src/claudesync.egg-info/SOURCES.txt +0 -17
- claudesync-0.2.6/src/claudesync.egg-info/entry_points.txt +0 -2
- {claudesync-0.2.6 → claudesync-0.2.8}/setup.cfg +0 -0
- {claudesync-0.2.6 → claudesync-0.2.8}/setup.py +0 -0
- {claudesync-0.2.6 → claudesync-0.2.8}/src/claudesync.egg-info/dependency_links.txt +0 -0
- {claudesync-0.2.6 → claudesync-0.2.8}/src/claudesync.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: claudesync
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: A tool to synchronize local files with Claude.ai projects
|
|
5
|
+
Author-email: Jahziah Wagner <jahziah.wagner+pypi@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Jahziah Wagner
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/jahwag/claudesync
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/jahwag/claudesync/issues
|
|
30
|
+
Classifier: Programming Language :: Python :: 3
|
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Requires-Python: >=3.7
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: Click
|
|
37
|
+
Requires-Dist: requests
|
|
38
|
+
Requires-Dist: watchdog
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
.oooooo. oooo .o8 .oooooo..o
|
|
42
|
+
d8P' `Y8b `888 "888 d8P' `Y8
|
|
43
|
+
888 888 .oooo. oooo oooo .oooo888 .ooooo. Y88bo. oooo ooo ooo. .oo. .ooooo.
|
|
44
|
+
888 888 `P )88b `888 `888 d88' `888 d88' `88b `"Y8888o. `88. .8' `888P"Y88b d88' `"Y8
|
|
45
|
+
888 888 .oP"888 888 888 888 888 888ooo888 `"Y88b `88..8' 888 888 888
|
|
46
|
+
`88b ooo 888 d8( 888 888 888 888 888 888 .o oo .d8P `888' 888 888 888 .o8
|
|
47
|
+
`Y8bood8P' o888o `Y888""8o `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""88888P' .8' o888o o888o `Y8bod8P'
|
|
48
|
+
.o..P'
|
|
49
|
+
`Y8P'
|
|
50
|
+
```
|
|
51
|
+

|
|
52
|
+
[](https://badge.fury.io/py/claudesync)
|
|
53
|
+
|
|
54
|
+
ClaudeSync is a powerful tool designed to seamlessly synchronize your local files with [Claude.ai](https://www.anthropic.com/claude) projects.
|
|
55
|
+
|
|
56
|
+
## Overview and Scope
|
|
57
|
+
|
|
58
|
+
ClaudeSync bridges the gap between your local development environment and Claude.ai's knowledge base. At a high level, the scope of ClaudeSync includes:
|
|
59
|
+
|
|
60
|
+
- Real-time synchronization with Claude.ai projects
|
|
61
|
+
- Command-line interface (CLI) for easy management
|
|
62
|
+
- Multiple organization and project support
|
|
63
|
+
- Automatic handling of file creation, modification, and deletion
|
|
64
|
+
- Intelligent file filtering based on .gitignore rules
|
|
65
|
+
- Configurable sync interval with cron job support
|
|
66
|
+
- Seamless integration with your existing workflow
|
|
67
|
+
|
|
68
|
+
## Roadmap
|
|
69
|
+
|
|
70
|
+
1. Enhanced support for large file synchronization
|
|
71
|
+
2. Improved conflict resolution mechanisms
|
|
72
|
+
3. GUI client for easier management
|
|
73
|
+
4. Integration with popular IDEs and text editors
|
|
74
|
+
5. Support for additional AI platforms beyond Claude.ai
|
|
75
|
+
|
|
76
|
+
## Quick Start
|
|
77
|
+
|
|
78
|
+
1. **Install ClaudeSync:**
|
|
79
|
+
```bash
|
|
80
|
+
pip install claudesync
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
2. **Login to Claude.ai:**
|
|
84
|
+
```bash
|
|
85
|
+
claudesync login claude.ai
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
3. **Select an organization:**
|
|
89
|
+
```bash
|
|
90
|
+
claudesync organization select
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
4. **Select or create a project:**
|
|
94
|
+
```bash
|
|
95
|
+
claudesync project select
|
|
96
|
+
# or
|
|
97
|
+
claudesync project create
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
5. **Start syncing:**
|
|
101
|
+
```bash
|
|
102
|
+
claudesync sync
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Advanced Usage
|
|
106
|
+
|
|
107
|
+
### Organization Management
|
|
108
|
+
- List organizations: `claudesync organization list`
|
|
109
|
+
- Select active organization: `claudesync organization select`
|
|
110
|
+
|
|
111
|
+
### Project Management
|
|
112
|
+
- List projects: `claudesync project ls`
|
|
113
|
+
- Create a new project: `claudesync project create`
|
|
114
|
+
- Archive a project: `claudesync project archive`
|
|
115
|
+
- Select active project: `claudesync project select`
|
|
116
|
+
|
|
117
|
+
### File Management
|
|
118
|
+
- List remote files: `claudesync ls`
|
|
119
|
+
- Sync files: `claudesync sync`
|
|
120
|
+
|
|
121
|
+
### Configuration
|
|
122
|
+
- View current status: `claudesync status`
|
|
123
|
+
|
|
124
|
+
### Scheduled Sync
|
|
125
|
+
Set up automatic syncing at regular intervals:
|
|
126
|
+
```bash
|
|
127
|
+
claudesync schedule
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Contributing
|
|
131
|
+
|
|
132
|
+
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more information.
|
|
133
|
+
|
|
134
|
+
## Communication Channels
|
|
135
|
+
|
|
136
|
+
- **Issues**: For bug reports and feature requests, please use our [GitHub Issues](https://github.com/jahwag/claudesync/issues).
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
ClaudeSync is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
141
|
+
|
|
142
|
+
## Related Projects
|
|
143
|
+
|
|
144
|
+
- [Claude.ai](https://www.anthropic.com/claude): The AI assistant that ClaudeSync integrates with.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
Made with ❤️ by the ClaudeSync team
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
```
|
|
2
|
+
.oooooo. oooo .o8 .oooooo..o
|
|
3
|
+
d8P' `Y8b `888 "888 d8P' `Y8
|
|
4
|
+
888 888 .oooo. oooo oooo .oooo888 .ooooo. Y88bo. oooo ooo ooo. .oo. .ooooo.
|
|
5
|
+
888 888 `P )88b `888 `888 d88' `888 d88' `88b `"Y8888o. `88. .8' `888P"Y88b d88' `"Y8
|
|
6
|
+
888 888 .oP"888 888 888 888 888 888ooo888 `"Y88b `88..8' 888 888 888
|
|
7
|
+
`88b ooo 888 d8( 888 888 888 888 888 888 .o oo .d8P `888' 888 888 888 .o8
|
|
8
|
+
`Y8bood8P' o888o `Y888""8o `V88V"V8P' `Y8bod88P" `Y8bod8P' 8""88888P' .8' o888o o888o `Y8bod8P'
|
|
9
|
+
.o..P'
|
|
10
|
+
`Y8P'
|
|
11
|
+
```
|
|
12
|
+

|
|
13
|
+
[](https://badge.fury.io/py/claudesync)
|
|
14
|
+
|
|
15
|
+
ClaudeSync is a powerful tool designed to seamlessly synchronize your local files with [Claude.ai](https://www.anthropic.com/claude) projects.
|
|
16
|
+
|
|
17
|
+
## Overview and Scope
|
|
18
|
+
|
|
19
|
+
ClaudeSync bridges the gap between your local development environment and Claude.ai's knowledge base. At a high level, the scope of ClaudeSync includes:
|
|
20
|
+
|
|
21
|
+
- Real-time synchronization with Claude.ai projects
|
|
22
|
+
- Command-line interface (CLI) for easy management
|
|
23
|
+
- Multiple organization and project support
|
|
24
|
+
- Automatic handling of file creation, modification, and deletion
|
|
25
|
+
- Intelligent file filtering based on .gitignore rules
|
|
26
|
+
- Configurable sync interval with cron job support
|
|
27
|
+
- Seamless integration with your existing workflow
|
|
28
|
+
|
|
29
|
+
## Roadmap
|
|
30
|
+
|
|
31
|
+
1. Enhanced support for large file synchronization
|
|
32
|
+
2. Improved conflict resolution mechanisms
|
|
33
|
+
3. GUI client for easier management
|
|
34
|
+
4. Integration with popular IDEs and text editors
|
|
35
|
+
5. Support for additional AI platforms beyond Claude.ai
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
1. **Install ClaudeSync:**
|
|
40
|
+
```bash
|
|
41
|
+
pip install claudesync
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
2. **Login to Claude.ai:**
|
|
45
|
+
```bash
|
|
46
|
+
claudesync login claude.ai
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. **Select an organization:**
|
|
50
|
+
```bash
|
|
51
|
+
claudesync organization select
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
4. **Select or create a project:**
|
|
55
|
+
```bash
|
|
56
|
+
claudesync project select
|
|
57
|
+
# or
|
|
58
|
+
claudesync project create
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
5. **Start syncing:**
|
|
62
|
+
```bash
|
|
63
|
+
claudesync sync
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Advanced Usage
|
|
67
|
+
|
|
68
|
+
### Organization Management
|
|
69
|
+
- List organizations: `claudesync organization list`
|
|
70
|
+
- Select active organization: `claudesync organization select`
|
|
71
|
+
|
|
72
|
+
### Project Management
|
|
73
|
+
- List projects: `claudesync project ls`
|
|
74
|
+
- Create a new project: `claudesync project create`
|
|
75
|
+
- Archive a project: `claudesync project archive`
|
|
76
|
+
- Select active project: `claudesync project select`
|
|
77
|
+
|
|
78
|
+
### File Management
|
|
79
|
+
- List remote files: `claudesync ls`
|
|
80
|
+
- Sync files: `claudesync sync`
|
|
81
|
+
|
|
82
|
+
### Configuration
|
|
83
|
+
- View current status: `claudesync status`
|
|
84
|
+
|
|
85
|
+
### Scheduled Sync
|
|
86
|
+
Set up automatic syncing at regular intervals:
|
|
87
|
+
```bash
|
|
88
|
+
claudesync schedule
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Contributing
|
|
92
|
+
|
|
93
|
+
We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for more information.
|
|
94
|
+
|
|
95
|
+
## Communication Channels
|
|
96
|
+
|
|
97
|
+
- **Issues**: For bug reports and feature requests, please use our [GitHub Issues](https://github.com/jahwag/claudesync/issues).
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
ClaudeSync is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
102
|
+
|
|
103
|
+
## Related Projects
|
|
104
|
+
|
|
105
|
+
- [Claude.ai](https://www.anthropic.com/claude): The AI assistant that ClaudeSync integrates with.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
Made with ❤️ by the ClaudeSync team
|
|
@@ -4,22 +4,23 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "claudesync"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
8
8
|
authors = [
|
|
9
9
|
{name = "Jahziah Wagner", email = "jahziah.wagner+pypi@gmail.com"},
|
|
10
10
|
]
|
|
11
11
|
description = "A tool to synchronize local files with Claude.ai projects"
|
|
12
|
+
license = {file = "LICENSE"}
|
|
12
13
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
14
|
+
requires-python = ">=3.7"
|
|
14
15
|
classifiers = [
|
|
15
16
|
"Programming Language :: Python :: 3",
|
|
16
17
|
"License :: OSI Approved :: MIT License",
|
|
17
18
|
"Operating System :: OS Independent",
|
|
18
19
|
]
|
|
19
20
|
dependencies = [
|
|
20
|
-
"
|
|
21
|
+
"Click",
|
|
21
22
|
"requests",
|
|
22
|
-
"
|
|
23
|
+
"watchdog",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
[project.urls]
|
|
@@ -27,7 +28,8 @@ dependencies = [
|
|
|
27
28
|
"Bug Tracker" = "https://github.com/jahwag/claudesync/issues"
|
|
28
29
|
|
|
29
30
|
[project.scripts]
|
|
30
|
-
claudesync = "claudesync.
|
|
31
|
+
claudesync = "claudesync.cli:cli"
|
|
31
32
|
|
|
32
33
|
[tool.setuptools.packages.find]
|
|
33
34
|
where = ["src"]
|
|
35
|
+
include = ["claudesync*"]
|
|
File without changes
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from crontab import CronTab
|
|
8
|
+
from .config_manager import ConfigManager
|
|
9
|
+
from .provider_factory import get_provider
|
|
10
|
+
from .exceptions import ConfigurationError, ProviderError
|
|
11
|
+
from .utils import calculate_checksum, get_local_files
|
|
12
|
+
|
|
13
|
+
def handle_errors(func):
|
|
14
|
+
@wraps(func)
|
|
15
|
+
def wrapper(*args, **kwargs):
|
|
16
|
+
try:
|
|
17
|
+
return func(*args, **kwargs)
|
|
18
|
+
except (ConfigurationError, ProviderError) as e:
|
|
19
|
+
click.echo(f"Error: {str(e)}")
|
|
20
|
+
return wrapper
|
|
21
|
+
|
|
22
|
+
def validate_and_get_provider(config, require_org=True):
|
|
23
|
+
active_provider = config.get('active_provider')
|
|
24
|
+
session_key = config.get('session_key')
|
|
25
|
+
if not active_provider or not session_key:
|
|
26
|
+
raise ConfigurationError("No active provider or session key. Please login first.")
|
|
27
|
+
if require_org and not config.get('active_organization_id'):
|
|
28
|
+
raise ConfigurationError("No active organization set. Please select an organization.")
|
|
29
|
+
return get_provider(active_provider, session_key)
|
|
30
|
+
|
|
31
|
+
def validate_and_store_local_path(config):
|
|
32
|
+
while True:
|
|
33
|
+
local_path = click.prompt("Enter the absolute path to your local project directory", type=str)
|
|
34
|
+
if os.path.isabs(local_path):
|
|
35
|
+
if os.path.exists(local_path):
|
|
36
|
+
config.set('local_path', local_path)
|
|
37
|
+
click.echo(f"Local path set to: {local_path}")
|
|
38
|
+
break
|
|
39
|
+
else:
|
|
40
|
+
click.echo("The specified path does not exist. Please enter a valid path.")
|
|
41
|
+
else:
|
|
42
|
+
click.echo("Please enter an absolute path.")
|
|
43
|
+
|
|
44
|
+
@click.group()
|
|
45
|
+
@click.pass_context
|
|
46
|
+
def cli(ctx):
|
|
47
|
+
"""ClaudeSync: Synchronize local files with ai projects."""
|
|
48
|
+
ctx.obj = ConfigManager()
|
|
49
|
+
|
|
50
|
+
@cli.command()
|
|
51
|
+
@click.argument('provider', required=False)
|
|
52
|
+
@click.pass_obj
|
|
53
|
+
@handle_errors
|
|
54
|
+
def login(config, provider):
|
|
55
|
+
"""
|
|
56
|
+
Authenticate with an AI provider.
|
|
57
|
+
|
|
58
|
+
If no provider is specified, lists available providers.
|
|
59
|
+
Otherwise, initiates the login process for the specified provider.
|
|
60
|
+
"""
|
|
61
|
+
providers = get_provider()
|
|
62
|
+
if not provider:
|
|
63
|
+
click.echo("Available providers:\n" + "\n".join(f" - {p}" for p in providers))
|
|
64
|
+
return
|
|
65
|
+
if provider not in providers:
|
|
66
|
+
click.echo(f"Error: Unknown provider '{provider}'. Available: {', '.join(providers)}")
|
|
67
|
+
return
|
|
68
|
+
provider_instance = get_provider(provider)
|
|
69
|
+
session_key = provider_instance.login()
|
|
70
|
+
config.set('session_key', session_key)
|
|
71
|
+
config.set('active_provider', provider)
|
|
72
|
+
click.echo("Logged in successfully.")
|
|
73
|
+
|
|
74
|
+
@cli.command()
|
|
75
|
+
@click.pass_obj
|
|
76
|
+
def logout(config):
|
|
77
|
+
"""
|
|
78
|
+
Log out from the current AI provider.
|
|
79
|
+
|
|
80
|
+
Clears all stored authentication and active selection data.
|
|
81
|
+
"""
|
|
82
|
+
for key in ['session_key', 'active_provider', 'active_organization_id']:
|
|
83
|
+
config.set(key, None)
|
|
84
|
+
click.echo("Logged out successfully.")
|
|
85
|
+
|
|
86
|
+
@cli.group()
|
|
87
|
+
def organization():
|
|
88
|
+
"""Manage ai organizations."""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@organization.command()
|
|
92
|
+
@click.pass_obj
|
|
93
|
+
@handle_errors
|
|
94
|
+
def list(config):
|
|
95
|
+
"""
|
|
96
|
+
List all available organizations.
|
|
97
|
+
|
|
98
|
+
Displays organizations the user has access to, including their names and IDs.
|
|
99
|
+
"""
|
|
100
|
+
provider = validate_and_get_provider(config, require_org=False)
|
|
101
|
+
organizations = provider.get_organizations()
|
|
102
|
+
if not organizations:
|
|
103
|
+
click.echo("No organizations found.")
|
|
104
|
+
else:
|
|
105
|
+
click.echo("Available organizations:")
|
|
106
|
+
for idx, org in enumerate(organizations, 1):
|
|
107
|
+
click.echo(f" {idx}. {org['name']} (ID: {org['id']})")
|
|
108
|
+
|
|
109
|
+
@organization.command()
|
|
110
|
+
@click.pass_obj
|
|
111
|
+
@handle_errors
|
|
112
|
+
def select(config):
|
|
113
|
+
"""
|
|
114
|
+
Set the active organization.
|
|
115
|
+
|
|
116
|
+
Prompts the user to choose from available organizations and sets it as active.
|
|
117
|
+
"""
|
|
118
|
+
provider = validate_and_get_provider(config, require_org=False)
|
|
119
|
+
organizations = provider.get_organizations()
|
|
120
|
+
if not organizations:
|
|
121
|
+
click.echo("No organizations found.")
|
|
122
|
+
return
|
|
123
|
+
click.echo("Available organizations:")
|
|
124
|
+
for idx, org in enumerate(organizations, 1):
|
|
125
|
+
click.echo(f" {idx}. {org['name']} (ID: {org['id']})")
|
|
126
|
+
selection = click.prompt("Enter the number of the organization to select", type=int)
|
|
127
|
+
if 1 <= selection <= len(organizations):
|
|
128
|
+
selected_org = organizations[selection - 1]
|
|
129
|
+
config.set('active_organization_id', selected_org['id'])
|
|
130
|
+
click.echo(f"Selected organization: {selected_org['name']} (ID: {selected_org['id']})")
|
|
131
|
+
else:
|
|
132
|
+
click.echo("Invalid selection. Please try again.")
|
|
133
|
+
|
|
134
|
+
@cli.group()
|
|
135
|
+
def project():
|
|
136
|
+
"""Manage ai projects within the active organization."""
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
@project.command()
|
|
140
|
+
@click.pass_obj
|
|
141
|
+
@handle_errors
|
|
142
|
+
def create(config):
|
|
143
|
+
"""
|
|
144
|
+
Create a new project in the active organization.
|
|
145
|
+
|
|
146
|
+
Prompts for project title and description, then creates the project and sets it as active.
|
|
147
|
+
Also prompts for the local directory to sync with the new project.
|
|
148
|
+
"""
|
|
149
|
+
provider = validate_and_get_provider(config)
|
|
150
|
+
active_organization_id = config.get('active_organization_id')
|
|
151
|
+
|
|
152
|
+
title = click.prompt("Enter the project title")
|
|
153
|
+
description = click.prompt("Enter the project description (optional)", default="")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
new_project = provider.create_project(active_organization_id, title, description)
|
|
157
|
+
click.echo(f"Project '{new_project['name']}' (uuid: {new_project['uuid']}) has been created successfully.")
|
|
158
|
+
|
|
159
|
+
config.set('active_project_id', new_project['uuid'])
|
|
160
|
+
config.set('active_project_name', new_project['name'])
|
|
161
|
+
click.echo(f"Active project set to: {new_project['name']} (uuid: {new_project['uuid']})")
|
|
162
|
+
|
|
163
|
+
validate_and_store_local_path(config)
|
|
164
|
+
|
|
165
|
+
except ProviderError as e:
|
|
166
|
+
click.echo(f"Failed to create project: {str(e)}")
|
|
167
|
+
|
|
168
|
+
@project.command()
|
|
169
|
+
@click.pass_obj
|
|
170
|
+
@handle_errors
|
|
171
|
+
def archive(config):
|
|
172
|
+
"""
|
|
173
|
+
Archive an existing project.
|
|
174
|
+
|
|
175
|
+
Lists active projects and allows the user to select one for archiving.
|
|
176
|
+
Archived projects are no longer available for syncing but can be viewed with the --all flag.
|
|
177
|
+
"""
|
|
178
|
+
provider = validate_and_get_provider(config)
|
|
179
|
+
active_organization_id = config.get('active_organization_id')
|
|
180
|
+
projects = provider.get_projects(active_organization_id, include_archived=False)
|
|
181
|
+
if not projects:
|
|
182
|
+
click.echo("No active projects found.")
|
|
183
|
+
return
|
|
184
|
+
click.echo("Available projects to archive:")
|
|
185
|
+
for idx, project in enumerate(projects, 1):
|
|
186
|
+
click.echo(f" {idx}. {project['name']} (ID: {project['id']})")
|
|
187
|
+
selection = click.prompt("Enter the number of the project to archive", type=int)
|
|
188
|
+
if 1 <= selection <= len(projects):
|
|
189
|
+
selected_project = projects[selection - 1]
|
|
190
|
+
if click.confirm(f"Are you sure you want to archive '{selected_project['name']}'?"):
|
|
191
|
+
provider.archive_project(active_organization_id, selected_project['id'])
|
|
192
|
+
click.echo(f"Project '{selected_project['name']}' has been archived.")
|
|
193
|
+
else:
|
|
194
|
+
click.echo("Invalid selection. Please try again.")
|
|
195
|
+
|
|
196
|
+
@project.command()
|
|
197
|
+
@click.pass_obj
|
|
198
|
+
@handle_errors
|
|
199
|
+
def select(config):
|
|
200
|
+
"""
|
|
201
|
+
Set the active project for syncing.
|
|
202
|
+
|
|
203
|
+
Lists available projects in the active organization and prompts user to select one.
|
|
204
|
+
Also prompts for the local directory to sync with the selected project.
|
|
205
|
+
"""
|
|
206
|
+
provider = validate_and_get_provider(config)
|
|
207
|
+
active_organization_id = config.get('active_organization_id')
|
|
208
|
+
projects = provider.get_projects(active_organization_id, include_archived=False)
|
|
209
|
+
if not projects:
|
|
210
|
+
click.echo("No active projects found.")
|
|
211
|
+
return
|
|
212
|
+
click.echo("Available projects:")
|
|
213
|
+
for idx, project in enumerate(projects, 1):
|
|
214
|
+
click.echo(f" {idx}. {project['name']} (ID: {project['id']})")
|
|
215
|
+
selection = click.prompt("Enter the number of the project to select", type=int)
|
|
216
|
+
if 1 <= selection <= len(projects):
|
|
217
|
+
selected_project = projects[selection - 1]
|
|
218
|
+
config.set('active_project_id', selected_project['id'])
|
|
219
|
+
config.set('active_project_name', selected_project['name'])
|
|
220
|
+
click.echo(f"Selected project: {selected_project['name']} (ID: {selected_project['id']})")
|
|
221
|
+
|
|
222
|
+
validate_and_store_local_path(config)
|
|
223
|
+
else:
|
|
224
|
+
click.echo("Invalid selection. Please try again.")
|
|
225
|
+
|
|
226
|
+
@project.command()
|
|
227
|
+
@click.option('-a', '--all', 'show_all', is_flag=True, help="Include archived projects in the list")
|
|
228
|
+
@click.pass_obj
|
|
229
|
+
@handle_errors
|
|
230
|
+
def ls(config, show_all):
|
|
231
|
+
"""
|
|
232
|
+
List all projects in the active organization.
|
|
233
|
+
|
|
234
|
+
Displays project names and IDs. Use --all flag to include archived projects.
|
|
235
|
+
"""
|
|
236
|
+
provider = validate_and_get_provider(config)
|
|
237
|
+
active_organization_id = config.get('active_organization_id')
|
|
238
|
+
projects = provider.get_projects(active_organization_id, include_archived=show_all)
|
|
239
|
+
if not projects:
|
|
240
|
+
click.echo("No projects found.")
|
|
241
|
+
else:
|
|
242
|
+
click.echo("Remote projects:")
|
|
243
|
+
for project in projects:
|
|
244
|
+
status = " (Archived)" if project.get('archived_at') else ""
|
|
245
|
+
click.echo(f" - {project['name']} (ID: {project['id']}){status}")
|
|
246
|
+
|
|
247
|
+
@cli.command()
|
|
248
|
+
@click.pass_obj
|
|
249
|
+
def status(config):
|
|
250
|
+
"""
|
|
251
|
+
Display current configuration status.
|
|
252
|
+
|
|
253
|
+
Shows active provider, organization, project, local sync path, and log level.
|
|
254
|
+
"""
|
|
255
|
+
for key in ['active_provider', 'active_organization_id', 'active_project_id', 'active_project_name', 'local_path', 'log_level']:
|
|
256
|
+
value = config.get(key)
|
|
257
|
+
click.echo(f"{key.replace('_', ' ').capitalize()}: {value or 'Not set'}")
|
|
258
|
+
|
|
259
|
+
@cli.command()
|
|
260
|
+
@click.pass_obj
|
|
261
|
+
@handle_errors
|
|
262
|
+
def ls(config):
|
|
263
|
+
"""
|
|
264
|
+
List files in the active remote project.
|
|
265
|
+
|
|
266
|
+
Displays file names, IDs, and creation dates for all files in the current ai project.
|
|
267
|
+
"""
|
|
268
|
+
provider = validate_and_get_provider(config)
|
|
269
|
+
active_organization_id = config.get('active_organization_id')
|
|
270
|
+
active_project_id = config.get('active_project_id')
|
|
271
|
+
files = provider.list_files(active_organization_id, active_project_id)
|
|
272
|
+
if not files:
|
|
273
|
+
click.echo("No files found in the active project.")
|
|
274
|
+
else:
|
|
275
|
+
click.echo(f"Files in project '{config.get('active_project_name')}' (ID: {active_project_id}):")
|
|
276
|
+
for file in files:
|
|
277
|
+
click.echo(f" - {file['file_name']} (ID: {file['uuid']}, Created: {file['created_at']})")
|
|
278
|
+
|
|
279
|
+
@cli.command()
|
|
280
|
+
@click.pass_obj
|
|
281
|
+
@handle_errors
|
|
282
|
+
def sync(config):
|
|
283
|
+
"""
|
|
284
|
+
Synchronize local files with the active remote project.
|
|
285
|
+
|
|
286
|
+
Compares local and remote files, uploading new or modified local files and updating changed remote files.
|
|
287
|
+
"""
|
|
288
|
+
provider = validate_and_get_provider(config)
|
|
289
|
+
active_organization_id = config.get('active_organization_id')
|
|
290
|
+
active_project_id = config.get('active_project_id')
|
|
291
|
+
local_path = config.get('local_path')
|
|
292
|
+
|
|
293
|
+
if not local_path:
|
|
294
|
+
click.echo("No local path set. Please select or create a project to set the local path.")
|
|
295
|
+
sys.exit(1)
|
|
296
|
+
|
|
297
|
+
if not os.path.exists(local_path):
|
|
298
|
+
click.echo(f"The configured local path does not exist: {local_path}")
|
|
299
|
+
click.echo("Please update the local path by selecting or creating a project.")
|
|
300
|
+
sys.exit(1)
|
|
301
|
+
|
|
302
|
+
remote_files = provider.list_files(active_organization_id, active_project_id)
|
|
303
|
+
local_files = get_local_files(local_path)
|
|
304
|
+
|
|
305
|
+
for local_file, local_checksum in local_files.items():
|
|
306
|
+
remote_file = next((rf for rf in remote_files if rf['file_name'] == local_file), None)
|
|
307
|
+
if remote_file:
|
|
308
|
+
remote_checksum = calculate_checksum(remote_file['content'])
|
|
309
|
+
if local_checksum != remote_checksum:
|
|
310
|
+
click.echo(f"Updating {local_file} on remote...")
|
|
311
|
+
for rf in remote_files:
|
|
312
|
+
if rf['file_name'] == local_file:
|
|
313
|
+
provider.delete_file(active_organization_id, active_project_id, rf['uuid'])
|
|
314
|
+
with open(os.path.join(local_path, local_file), 'r', encoding='utf-8') as file:
|
|
315
|
+
content = file.read()
|
|
316
|
+
provider.upload_file(active_organization_id, active_project_id, local_file, content)
|
|
317
|
+
else:
|
|
318
|
+
click.echo(f"Uploading new file {local_file} to remote...")
|
|
319
|
+
with open(os.path.join(local_path, local_file), 'r', encoding='utf-8') as file:
|
|
320
|
+
content = file.read()
|
|
321
|
+
provider.upload_file(active_organization_id, active_project_id, local_file, content)
|
|
322
|
+
|
|
323
|
+
click.echo("Sync completed successfully.")
|
|
324
|
+
|
|
325
|
+
@cli.command()
|
|
326
|
+
@click.pass_obj
|
|
327
|
+
@click.option('--interval', type=int, default=5, prompt='Enter sync interval in minutes')
|
|
328
|
+
@handle_errors
|
|
329
|
+
def schedule(config, interval):
|
|
330
|
+
"""
|
|
331
|
+
Set up automated synchronization at regular intervals.
|
|
332
|
+
|
|
333
|
+
Creates a cron job (Unix/Linux/macOS) or scheduled task (Windows) to run sync command periodically.
|
|
334
|
+
Prompts for sync interval in minutes.
|
|
335
|
+
"""
|
|
336
|
+
claudesync_path = shutil.which('claudesync')
|
|
337
|
+
if not claudesync_path:
|
|
338
|
+
click.echo("Error: claudesync not found in PATH. Please ensure it's installed correctly.")
|
|
339
|
+
sys.exit(1)
|
|
340
|
+
|
|
341
|
+
if sys.platform.startswith('win'):
|
|
342
|
+
click.echo("Windows Task Scheduler setup:")
|
|
343
|
+
command = f'schtasks /create /tn "ClaudeSync" /tr "{claudesync_path} sync" /sc minute /mo {interval}'
|
|
344
|
+
click.echo(f"Run this command to create the task:\n{command}")
|
|
345
|
+
click.echo("\nTo remove the task, run: schtasks /delete /tn \"ClaudeSync\" /f")
|
|
346
|
+
else:
|
|
347
|
+
# Unix-like systems (Linux, macOS)
|
|
348
|
+
cron = CronTab(user=True)
|
|
349
|
+
job = cron.new(command=f'{claudesync_path} sync')
|
|
350
|
+
job.minute.every(interval)
|
|
351
|
+
|
|
352
|
+
cron.write()
|
|
353
|
+
click.echo(f"Cron job created successfully! It will run every {interval} minutes.")
|
|
354
|
+
click.echo("\nTo remove the cron job, run: crontab -e and remove the line for ClaudeSync")
|
|
355
|
+
|
|
356
|
+
if __name__ == '__main__':
|
|
357
|
+
cli()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
class ConfigManager:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.config_dir = Path.home() / '.claudesync'
|
|
8
|
+
self.config_file = self.config_dir / 'config.json'
|
|
9
|
+
self.config = self._load_config()
|
|
10
|
+
|
|
11
|
+
def _load_config(self):
|
|
12
|
+
if not self.config_file.exists():
|
|
13
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
return {'log_level': 'INFO'} # Default log level
|
|
15
|
+
with open(self.config_file, 'r') as f:
|
|
16
|
+
config = json.load(f)
|
|
17
|
+
if 'log_level' not in config:
|
|
18
|
+
config['log_level'] = 'INFO' # Set default if not present
|
|
19
|
+
return config
|
|
20
|
+
|
|
21
|
+
def _save_config(self):
|
|
22
|
+
with open(self.config_file, 'w') as f:
|
|
23
|
+
json.dump(self.config, f, indent=2)
|
|
24
|
+
|
|
25
|
+
def get(self, key, default=None):
|
|
26
|
+
return self.config.get(key, default)
|
|
27
|
+
|
|
28
|
+
def set(self, key, value):
|
|
29
|
+
self.config[key] = value
|
|
30
|
+
self._save_config()
|