claudesync 0.2.5__tar.gz → 0.2.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: claudesync
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: A tool to synchronize local files with Claude.ai projects
5
5
  Author-email: Jahziah Wagner <jahziah.wagner+pypi@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/jahwag/claudesync
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "claudesync"
7
- version = "0.2.5"
7
+ version = "0.2.7"
8
8
  authors = [
9
9
  {name = "Jahziah Wagner", email = "jahziah.wagner+pypi@gmail.com"},
10
10
  ]
@@ -1,11 +1,13 @@
1
1
  import os
2
+ import mimetypes
3
+ import time
2
4
  from watchdog.events import FileSystemEventHandler
3
5
  from .debounce import DebounceHandler
4
6
  from .gitignore_utils import load_gitignore, should_ignore
5
7
  import requests
6
8
 
7
9
  class FileUploadHandler(FileSystemEventHandler):
8
- def __init__(self, api_endpoint, session_key, base_path, delay=5):
10
+ def __init__(self, api_endpoint, session_key, base_path, delay=5, max_file_size=1024*320): # 320 KB limit or roughly 4k lines
9
11
  self.api_endpoint = api_endpoint
10
12
  self.session_key = session_key
11
13
  self.base_path = os.path.abspath(base_path)
@@ -19,6 +21,11 @@ class FileUploadHandler(FileSystemEventHandler):
19
21
  self.debouncer = DebounceHandler(delay)
20
22
  self.gitignore = load_gitignore(self.base_path)
21
23
  self.log_callback = None
24
+ self.max_file_size = max_file_size
25
+ self.backoff_time = 1
26
+ self.max_backoff_time = 60
27
+ self.session_expiration_time = 180 # 3 minutes
28
+ self.session_expiration_start = None
22
29
 
23
30
  def log(self, message):
24
31
  if self.log_callback:
@@ -35,46 +42,72 @@ class FileUploadHandler(FileSystemEventHandler):
35
42
  self.debouncer.debounce(self.upload_file, event.src_path)
36
43
 
37
44
  def should_ignore_file(self, file_path):
38
- # Check if the file is in the .git directory
39
- rel_path = os.path.relpath(file_path, self.base_path)
40
- if rel_path.startswith('.git' + os.path.sep) or rel_path == '.git':
41
- return True
45
+ # Check if the file is in the .git directory
46
+ rel_path = os.path.relpath(file_path, self.base_path)
47
+ if rel_path.startswith('.git' + os.path.sep) or rel_path == '.git':
48
+ return True
49
+
50
+ # Check file size
51
+ if os.path.getsize(file_path) > self.max_file_size:
52
+ self.log(f"Ignoring large file: {file_path}")
53
+ return True
54
+
55
+ # Check file type
56
+ mime_type, _ = mimetypes.guess_type(file_path)
57
+ if mime_type and not mime_type.startswith('text/'):
58
+ self.log(f"Ignoring non-text file: {file_path}")
59
+ return True
42
60
 
43
- # Check if the file should be ignored based on .gitignore
44
- return should_ignore(self.gitignore, file_path, self.base_path)
61
+ # Check if the file should be ignored based on .gitignore
62
+ return should_ignore(self.gitignore, file_path, self.base_path)
45
63
 
46
64
  def api_request(self, method, url, **kwargs):
47
- try:
48
- response = requests.request(method, url, headers=self.headers, cookies=self.cookies, **kwargs)
49
- response.raise_for_status()
50
- return response.json() if response.text else None
51
- except requests.RequestException as e:
52
- print(f"API request error: {str(e)}")
53
- return None
65
+ while True:
66
+ try:
67
+ response = requests.request(method, url, headers=self.headers, cookies=self.cookies, **kwargs)
68
+ if response.status_code == 403:
69
+ if self.session_expiration_start is None:
70
+ self.session_expiration_start = time.time()
71
+ elif time.time() - self.session_expiration_start > self.session_expiration_time:
72
+ self.log("Session key has likely expired. Please restart ClaudeSync with a new session key.")
73
+ raise SystemExit(1)
74
+
75
+ self.log(f"Received 403 error. Backing off for {self.backoff_time} seconds.")
76
+ time.sleep(self.backoff_time)
77
+ self.backoff_time = min(self.backoff_time * 2, self.max_backoff_time)
78
+ else:
79
+ self.session_expiration_start = None
80
+ self.backoff_time = 1
81
+ response.raise_for_status()
82
+ return response.json() if response.text else None
83
+ except requests.RequestException as e:
84
+ if response.status_code != 403:
85
+ self.log(f"API request error: {str(e)}")
86
+ return None
54
87
 
55
88
  def get_documents(self):
56
89
  return self.api_request('GET', self.api_endpoint) or []
57
90
 
58
91
  def delete_document(self, uuid):
59
92
  if self.api_request('DELETE', f"{self.api_endpoint}/{uuid}"):
60
- print(f"Deleted document: {uuid}")
93
+ self.log(f"Deleted document: {uuid}")
61
94
 
62
95
  def delete_all_documents(self):
63
96
  for doc in self.get_documents():
64
97
  self.delete_document(doc['uuid'])
65
- print("All documents deleted.")
98
+ self.log("All documents deleted.")
66
99
 
67
100
  def upload_file(self, file_path):
68
101
  if not os.path.isfile(file_path) or self.should_ignore_file(file_path):
69
102
  return
70
103
  if os.path.getsize(file_path) == 0:
71
- print(f"Skipping empty file: {file_path}")
104
+ self.log(f"Skipping empty file: {file_path}")
72
105
  return
73
106
  try:
74
107
  with open(file_path, 'r', encoding='utf-8') as file:
75
108
  content = file.read()
76
109
  if not content.strip():
77
- print(f"Skipping file with only whitespace: {file_path}")
110
+ self.log(f"Skipping file with only whitespace: {file_path}")
78
111
  return
79
112
 
80
113
  rel_path = os.path.relpath(file_path, self.base_path)
@@ -87,5 +120,4 @@ class FileUploadHandler(FileSystemEventHandler):
87
120
  if self.api_request('POST', self.api_endpoint, json=payload):
88
121
  self.log(f"Uploaded: {file_name}")
89
122
  except Exception as e:
90
- print(f"Error processing file {file_path}: {str(e)}")
91
-
123
+ self.log(f"Error processing file {file_path}: {str(e)}")
@@ -0,0 +1,163 @@
1
+ import blessed
2
+ import time
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from watchdog.observers import Observer
8
+ from .manual_auth import get_session_key, get_or_update_config_value, get_config, save_config
9
+ from .file_handler import FileUploadHandler
10
+ from .api_utils import fetch_user_id, fetch_projects, select_project
11
+
12
+ class ClaudeSyncTUI:
13
+ def __init__(self, session_key, watch_dir, user_id, project_id, delay):
14
+ self.term = blessed.Terminal()
15
+ self.session_key = session_key
16
+ self.watch_dir = watch_dir
17
+ self.user_id = user_id
18
+ self.project_id = project_id
19
+ self.delay = delay
20
+ self.log_messages = []
21
+ self.observer = None
22
+ self.handler = None
23
+ self.scroll_position = 0
24
+ self.max_log_lines = 1000
25
+ self.auto_scroll = True
26
+ self.is_syncing = False
27
+
28
+ def setup(self):
29
+ if not self.user_id:
30
+ self.user_id = fetch_user_id(self.session_key)
31
+ if not self.project_id:
32
+ projects = fetch_projects(self.user_id, self.session_key)
33
+ self.project_id = select_project(projects, self.user_id, self.session_key)
34
+
35
+ api_endpoint = f"https://claude.ai/api/organizations/{self.user_id}/projects/{self.project_id}/docs"
36
+ self.handler = FileUploadHandler(api_endpoint, self.session_key, self.watch_dir, self.delay)
37
+ self.handler.log_callback = self.add_log_message
38
+
39
+ self.observer = Observer()
40
+ self.observer.schedule(self.handler, self.watch_dir, recursive=True)
41
+
42
+ def initial_sync(self):
43
+ self.is_syncing = True
44
+ self.add_log_message("Starting initial synchronization...")
45
+ for root, dirs, files in os.walk(self.watch_dir):
46
+ for file in files:
47
+ file_path = os.path.join(root, file)
48
+ if not self.handler.should_ignore_file(file_path):
49
+ self.handler.upload_file(file_path)
50
+ self.draw() # Redraw the TUI after each file upload
51
+ self.add_log_message("Initial synchronization completed.")
52
+ self.is_syncing = False
53
+
54
+ def add_log_message(self, message):
55
+ self.log_messages.append(message)
56
+ if len(self.log_messages) > self.max_log_lines:
57
+ self.log_messages.pop(0)
58
+ if self.auto_scroll:
59
+ self.scroll_to_bottom()
60
+
61
+ def scroll_to_bottom(self):
62
+ self.scroll_position = max(0, len(self.log_messages) - (self.term.height - 11))
63
+
64
+ def draw(self):
65
+ print(self.term.clear())
66
+ print(self.term.move_y(0) + self.term.black_on_skyblue(self.term.center('ClaudeSync TUI')))
67
+
68
+ print(self.term.move_y(2) + f"Watching directory: {self.watch_dir}")
69
+ print(f"Upload delay: {self.delay} seconds")
70
+ print(f"User ID: {self.user_id}")
71
+ print(f"Project ID: {self.project_id}")
72
+ print(f"Auto-scroll: {'ON' if self.auto_scroll else 'OFF'}")
73
+
74
+ if self.is_syncing:
75
+ print(self.term.move_y(7) + self.term.red("Initial sync in progress..."))
76
+
77
+ print(self.term.move_y(8) + self.term.black_on_skyblue(self.term.center('Recent Activity')))
78
+
79
+ log_height = self.term.height - 12
80
+ visible_logs = self.log_messages[self.scroll_position:self.scroll_position + log_height]
81
+ for i, message in enumerate(visible_logs):
82
+ print(self.term.move_y(10 + i) + message)
83
+
84
+ print(self.term.move_y(self.term.height - 1) + "Press 'q' to quit, 'j'/'k' to scroll, 'a' to toggle auto-scroll")
85
+
86
+ def run(self):
87
+ with self.term.cbreak(), self.term.hidden_cursor():
88
+ self.draw() # Initial draw before starting sync
89
+ self.initial_sync()
90
+ self.observer.start()
91
+ while True:
92
+ self.draw()
93
+ inp = self.term.inkey(timeout=0.1)
94
+ if inp == 'q':
95
+ break
96
+ elif inp == 'j':
97
+ self.auto_scroll = False
98
+ self.scroll_position = min(len(self.log_messages) - 1, self.scroll_position + 1)
99
+ elif inp == 'k':
100
+ self.auto_scroll = False
101
+ self.scroll_position = max(0, self.scroll_position - 1)
102
+ elif inp == 'a':
103
+ self.auto_scroll = not self.auto_scroll
104
+ if self.auto_scroll:
105
+ self.scroll_to_bottom()
106
+
107
+ self.observer.stop()
108
+ self.observer.join()
109
+
110
+
111
+ def main():
112
+ parser = argparse.ArgumentParser(description="Sync local files with Claude.ai projects.")
113
+ parser.add_argument("--session-key", help="Session key for authentication")
114
+ parser.add_argument("--watch-dir", default=".", help="Directory to watch for changes")
115
+ parser.add_argument("--user-id", help="User ID for Claude API")
116
+ parser.add_argument("--project-id", help="Project ID for Claude API")
117
+ parser.add_argument("--delay", type=int, help="Delay in seconds before uploading")
118
+ args = parser.parse_args()
119
+
120
+ config = get_config()
121
+
122
+ # Get or update session key
123
+ session_key = args.session_key or get_session_key()
124
+
125
+ # Get or update watch directory
126
+ watch_dir = args.watch_dir or get_or_update_config_value('watch_dir', "Enter the directory to watch", ".")
127
+
128
+ # Get or fetch user ID
129
+ user_id = args.user_id or config.get('user_id')
130
+ if not user_id:
131
+ print("Fetching user ID...")
132
+ user_id = fetch_user_id(session_key)
133
+ config['user_id'] = user_id
134
+ save_config(config)
135
+ print(f"User ID fetched and stored: {user_id}")
136
+
137
+ # Get or select project ID
138
+ project_id = args.project_id or config.get('project_id')
139
+ if not project_id:
140
+ print("No project ID found. Fetching projects...")
141
+ projects = fetch_projects(user_id, session_key)
142
+ project_id = select_project(projects, user_id, session_key)
143
+ config['project_id'] = project_id
144
+ save_config(config)
145
+ print(f"Project ID selected and stored: {project_id}")
146
+ else:
147
+ use_stored = input(f"Found stored project ID: {project_id}. Use it? (y/n): ").strip().lower()
148
+ if use_stored != 'y':
149
+ projects = fetch_projects(user_id, session_key)
150
+ project_id = select_project(projects, user_id, session_key)
151
+ config['project_id'] = project_id
152
+ save_config(config)
153
+ print(f"New project ID selected and stored: {project_id}")
154
+
155
+ # Get or update delay
156
+ delay = args.delay or int(get_or_update_config_value('delay', "Enter the delay in seconds before uploading", "5"))
157
+
158
+ tui = ClaudeSyncTUI(session_key, watch_dir, user_id, project_id, delay)
159
+ tui.setup()
160
+ tui.run()
161
+
162
+ if __name__ == "__main__":
163
+ main()
@@ -0,0 +1,77 @@
1
+ import webbrowser
2
+ import os
3
+ import json
4
+
5
+ CONFIG_DIR = os.path.expanduser('~/.claudesync')
6
+ CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.json')
7
+
8
+ def get_config():
9
+ if os.path.exists(CONFIG_FILE):
10
+ with open(CONFIG_FILE, 'r') as f:
11
+ return json.load(f)
12
+ return {}
13
+
14
+ def save_config(config):
15
+ os.makedirs(CONFIG_DIR, exist_ok=True)
16
+ with open(CONFIG_FILE, 'w') as f:
17
+ json.dump(config, f, indent=2)
18
+
19
+ def get_session_key():
20
+ config = get_config()
21
+ stored_session_key = config.get('sessionKey')
22
+
23
+ if stored_session_key:
24
+ use_stored = input(f"Found stored sessionKey. Use it? (y/n): ").strip().lower()
25
+ if use_stored == 'y':
26
+ return stored_session_key
27
+
28
+ print("To obtain your session key, please follow these steps:")
29
+ print("1. Open your web browser and go to https://claude.ai")
30
+ print("2. Log in to your Claude account if you haven't already")
31
+ print("3. Once logged in, open your browser's developer tools:")
32
+ print(" - Chrome/Edge: Press F12 or Ctrl+Shift+I (Cmd+Option+I on Mac)")
33
+ print(" - Firefox: Press F12 or Ctrl+Shift+I (Cmd+Option+I on Mac)")
34
+ print(" - Safari: Enable developer tools in Preferences > Advanced, then press Cmd+Option+I")
35
+ print("4. In the developer tools, go to the 'Application' tab (Chrome/Edge) or 'Storage' tab (Firefox)")
36
+ print("5. In the left sidebar, expand 'Cookies' and select 'https://claude.ai'")
37
+ print("6. Find the cookie named 'sessionKey' and copy its value")
38
+
39
+ try:
40
+ webbrowser.open("https://claude.ai")
41
+ except:
42
+ print("Unable to automatically open the browser. Please navigate to https://claude.ai manually.")
43
+
44
+ while True:
45
+ session_key = input("Please enter your sessionKey value: ").strip()
46
+ if session_key:
47
+ config['sessionKey'] = session_key
48
+ save_config(config)
49
+ print(f"SessionKey stored in {CONFIG_FILE}")
50
+ return session_key
51
+ else:
52
+ print("Session key cannot be empty. Please try again.")
53
+
54
+ def get_or_update_config_value(key, prompt, current_value=None):
55
+ config = get_config()
56
+ stored_value = config.get(key, current_value)
57
+
58
+ if stored_value is not None:
59
+ use_stored = input(f"Found stored {key}: {stored_value}. Use it? (y/n): ").strip().lower()
60
+ if use_stored == 'y':
61
+ return stored_value
62
+
63
+ while True:
64
+ new_value = input(f"{prompt}: ").strip()
65
+ if new_value:
66
+ config[key] = new_value
67
+ save_config(config)
68
+ print(f"{key} stored in {CONFIG_FILE}")
69
+ return new_value
70
+ elif current_value is not None:
71
+ return current_value
72
+ else:
73
+ print(f"{key} cannot be empty. Please try again.")
74
+
75
+ if __name__ == "__main__":
76
+ session_key = get_session_key()
77
+ print(f"Session key obtained: {session_key}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: claudesync
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: A tool to synchronize local files with Claude.ai projects
5
5
  Author-email: Jahziah Wagner <jahziah.wagner+pypi@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/jahwag/claudesync
@@ -1,100 +0,0 @@
1
- import blessed
2
- import time
3
- import argparse
4
- import json
5
- import os
6
- import sys
7
- from watchdog.observers import Observer
8
- from .file_handler import FileUploadHandler
9
- from .api_utils import fetch_user_id, fetch_projects, select_project, create_project
10
-
11
- from .manual_auth import get_session_key
12
-
13
- class ClaudeSyncTUI:
14
- def __init__(self, session_key, watch_dir, user_id, project_id, delay):
15
- self.term = blessed.Terminal()
16
- self.session_key = session_key
17
- self.watch_dir = watch_dir
18
- self.user_id = user_id
19
- self.project_id = project_id
20
- self.delay = delay
21
- self.log_messages = []
22
- self.observer = None
23
- self.handler = None
24
-
25
- def setup(self):
26
- if not self.user_id:
27
- self.user_id = fetch_user_id(self.session_key)
28
- if not self.project_id:
29
- projects = fetch_projects(self.user_id, self.session_key)
30
- self.project_id = select_project(projects, self.user_id, self.session_key)
31
-
32
- api_endpoint = f"https://claude.ai/api/organizations/{self.user_id}/projects/{self.project_id}/docs"
33
- self.handler = FileUploadHandler(api_endpoint, self.session_key, self.watch_dir, self.delay)
34
- self.handler.log_callback = self.add_log_message
35
-
36
- self.observer = Observer()
37
- self.observer.schedule(self.handler, self.watch_dir, recursive=True)
38
-
39
- def add_log_message(self, message):
40
- self.log_messages.append(message)
41
- if len(self.log_messages) > 10:
42
- self.log_messages.pop(0)
43
-
44
- def draw(self):
45
- print(self.term.clear())
46
- print(self.term.move_y(0) + self.term.black_on_skyblue(self.term.center('ClaudeSync TUI')))
47
-
48
- print(self.term.move_y(2) + f"Watching directory: {self.watch_dir}")
49
- print(f"Upload delay: {self.delay} seconds")
50
- print(f"User ID: {self.user_id}")
51
- print(f"Project ID: {self.project_id}")
52
-
53
- print(self.term.move_y(7) + self.term.black_on_skyblue(self.term.center('Recent Activity')))
54
- for i, message in enumerate(self.log_messages):
55
- print(self.term.move_y(9 + i) + message)
56
-
57
- print(self.term.move_y(20) + "Press 'q' to quit")
58
-
59
- def run(self):
60
- with self.term.cbreak(), self.term.hidden_cursor():
61
- self.observer.start()
62
- while True:
63
- self.draw()
64
- inp = self.term.inkey(timeout=1)
65
- if inp == 'q':
66
- break
67
- self.observer.stop()
68
- self.observer.join()
69
-
70
- def main():
71
- parser = argparse.ArgumentParser(description="Sync local files with Claude.ai projects.")
72
- parser.add_argument("--session-key", help="Session key for authentication")
73
- parser.add_argument("--watch-dir", default=".", help="Directory to watch for changes")
74
- parser.add_argument("--user-id", help="User ID for Claude API (optional, will be fetched if not provided)")
75
- parser.add_argument("--project-id", help="Project ID for Claude API")
76
- parser.add_argument("--delay", type=int, default=5, help="Delay in seconds before uploading (default: 5)")
77
- args = parser.parse_args()
78
-
79
- # Load config from file if it exists
80
- config = {}
81
- if os.path.exists('config.json'):
82
- with open('config.json', 'r') as f:
83
- config = json.load(f)
84
-
85
- # If session key is not provided, use the manual input method
86
- session_key = args.session_key
87
- if not session_key:
88
- session_key = get_session_key()
89
-
90
- watch_dir = args.watch_dir
91
- user_id = args.user_id or config.get('user_id')
92
- project_id = args.project_id or config.get('project_id')
93
- delay = args.delay
94
-
95
- tui = ClaudeSyncTUI(session_key, watch_dir, user_id, project_id, delay)
96
- tui.setup()
97
- tui.run()
98
-
99
- if __name__ == "__main__":
100
- main()
@@ -1,30 +0,0 @@
1
- import webbrowser
2
-
3
- def get_session_key():
4
- print("To obtain your session key, please follow these steps:")
5
- print("1. Open your web browser and go to https://claude.ai")
6
- print("2. Log in to your Claude account if you haven't already")
7
- print("3. Once logged in, open your browser's developer tools:")
8
- print(" - Chrome/Edge: Press F12 or Ctrl+Shift+I (Cmd+Option+I on Mac)")
9
- print(" - Firefox: Press F12 or Ctrl+Shift+I (Cmd+Option+I on Mac)")
10
- print(" - Safari: Enable developer tools in Preferences > Advanced, then press Cmd+Option+I")
11
- print("4. In the developer tools, go to the 'Application' tab (Chrome/Edge) or 'Storage' tab (Firefox)")
12
- print("5. In the left sidebar, expand 'Cookies' and select 'https://claude.ai'")
13
- print("6. Find the cookie named 'sessionKey' and copy its value")
14
-
15
- # Attempt to open the Claude.ai website for the user
16
- try:
17
- webbrowser.open("https://claude.ai")
18
- except:
19
- print("Unable to automatically open the browser. Please navigate to https://claude.ai manually. ")
20
-
21
- while True:
22
- session_key = input("Please enter your sessionKey value: ").strip()
23
- if session_key:
24
- return session_key
25
- else:
26
- print("Session key cannot be empty. Please try again.")
27
-
28
- if __name__ == "__main__":
29
- session_key = get_session_key()
30
- print(f"Session key obtained: {session_key}")
File without changes
File without changes
File without changes
File without changes