Skip to content

Commit

Permalink
Submodules, file subsets and chats&messages (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
jahwag authored Aug 21, 2024
1 parent 5dee612 commit fbf9d1b
Show file tree
Hide file tree
Showing 17 changed files with 994 additions and 287 deletions.
15 changes: 9 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "claudesync"
version = "0.5.1"
version = "0.5.2"
authors = [
{name = "Jahziah Wagner", email = "[email protected]"},
]
Expand All @@ -14,11 +14,14 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"Click>=8.1.7",
"pathspec>=0.12.1",
"crontab>=1.0.1",
"click_completion>=0.5.2",
"tqdm>=4.66.4",
"click==8.1.7",
"click_completion==0.5.2",
"pathspec==0.12.1",
"pytest==8.3.2",
"python_crontab==3.2.0",
"setuptools==73.0.1",
"sseclient_py==1.8.0",
"tqdm==4.66.5",
]
keywords = [
"sync",
Expand Down
11 changes: 7 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Click>=8.1.7
pathspec>=0.12.1
crontab>=1.0.1
click>=8.1.7
click_completion>=0.5.2
tqdm>=4.66.4
pathspec>=0.12.1
pytest>=8.3.2
python_crontab>=3.2.0
setuptools>=73.0.1
sseclient_py>=1.8.0
tqdm>=4.66.5
pytest-cov>=5.0.0
61 changes: 61 additions & 0 deletions src/claudesync/cli/category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import click
from ..utils import handle_errors


@click.group()
def category():
"""Manage file categories."""
pass


@category.command()
@click.argument("name")
@click.option("--description", required=True, help="Description of the category")
@click.option(
"--patterns", required=True, multiple=True, help="File patterns for the category"
)
@click.pass_obj
@handle_errors
def add(config, name, description, patterns):
"""Add a new file category."""
config.add_file_category(name, description, list(patterns))
click.echo(f"File category '{name}' added successfully.")


@category.command()
@click.argument("name")
@click.pass_obj
@handle_errors
def remove(config, name):
"""Remove a file category."""
config.remove_file_category(name)
click.echo(f"File category '{name}' removed successfully.")


@category.command()
@click.argument("name")
@click.option("--description", help="New description for the category")
@click.option("--patterns", multiple=True, help="New file patterns for the category")
@click.pass_obj
@handle_errors
def update(config, name, description, patterns):
"""Update an existing file category."""
config.update_file_category(name, description, list(patterns) if patterns else None)
click.echo(f"File category '{name}' updated successfully.")


@category.command()
@click.pass_obj
@handle_errors
def ls(config):
"""List all file categories."""
categories = config.get("file_categories", {})
if not categories:
click.echo("No file categories defined.")
else:
for name, data in categories.items():
click.echo(f"\nCategory: {name}")
click.echo(f"Description: {data['description']}")
click.echo("Patterns:")
for pattern in data["patterns"]:
click.echo(f" - {pattern}")
196 changes: 196 additions & 0 deletions src/claudesync/cli/chat.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

import click
import logging
from ..exceptions import ProviderError
Expand Down Expand Up @@ -136,3 +138,197 @@ def confirm_and_delete_chat(provider, organization_id, chat):
click.echo(f"Successfully deleted chat: {chat.get('name', 'Unnamed')}")
else:
click.echo(f"Failed to delete chat: {chat.get('name', 'Unnamed')}")


@chat.command()
@click.option("--name", default="", help="Name of the chat conversation")
@click.option("--project", help="UUID of the project to associate the chat with")
@click.pass_obj
@handle_errors
def create(config, name, project):
"""Create a new chat conversation on the active provider."""
provider = validate_and_get_provider(config)
organization_id = config.get("active_organization_id")
active_project_id = config.get("active_project_id")
active_project_name = config.get("active_project_name")
local_path = config.get("local_path")

if not organization_id:
click.echo("No active organization set.")
return

if not project:
project = select_project(
active_project_id,
active_project_name,
local_path,
organization_id,
provider,
)
if project is None:
return

try:
new_chat = provider.create_chat(
organization_id, chat_name=name, project_uuid=project
)
click.echo(f"Created new chat conversation: {new_chat['uuid']}")
if name:
click.echo(f"Chat name: {name}")
click.echo(f"Associated project: {project}")
except Exception as e:
click.echo(f"Failed to create chat conversation: {str(e)}")


@chat.command()
@click.argument("message", nargs=-1, required=True)
@click.option("--chat", help="UUID of the chat to send the message to")
@click.option("--timezone", default="UTC", help="Timezone for the message")
@click.pass_obj
@handle_errors
def send(config, message, chat, timezone):
"""Send a message to a specified chat or create a new chat and send the message."""
provider = validate_and_get_provider(config)
organization_id = config.get("active_organization_id")
active_project_id = config.get("active_project_id")
active_project_name = config.get("active_project_name")
local_path = config.get("local_path")

if not organization_id:
click.echo("No active organization set.")
return

message = " ".join(message) # Join all message parts into a single string

try:
chat = create_chat(
active_project_id,
active_project_name,
chat,
local_path,
organization_id,
provider,
)
if chat is None:
return

# Send message and process the streaming response
for event in provider.send_message(organization_id, chat, message, timezone):
if "completion" in event:
click.echo(event["completion"], nl=False)
elif "content" in event:
click.echo(event["content"], nl=False)
elif "error" in event:
click.echo(f"\nError: {event['error']}")
elif "message_limit" in event:
click.echo(
f"\nRemaining messages: {event['message_limit']['remaining']}"
)

click.echo() # Print a newline at the end of the response

except Exception as e:
click.echo(f"Failed to send message: {str(e)}")


def create_chat(
active_project_id, active_project_name, chat, local_path, organization_id, provider
):
if not chat:
selected_project = select_project(
active_project_id,
active_project_name,
local_path,
organization_id,
provider,
)
if selected_project is None:
return

# Create a new chat with the selected project
new_chat = provider.create_chat(organization_id, project_uuid=selected_project)
chat = new_chat["uuid"]
click.echo(f"New chat created with ID: {chat}")
return chat


def select_project(
active_project_id, active_project_name, local_path, organization_id, provider
):
all_projects = provider.get_projects(organization_id)
if not all_projects:
click.echo("No projects found in the active organization.")
return None

# Filter projects to include only the active project and its submodules
filtered_projects = [
p
for p in all_projects
if p["id"] == active_project_id
or (
p["name"].startswith(f"{active_project_name}-SubModule-")
and not p.get("archived_at")
)
]

if not filtered_projects:
click.echo("No active project or related submodules found.")
return None

# Determine the current working directory
current_dir = os.path.abspath(os.getcwd())

default_project = get_default_project(
active_project_id,
active_project_name,
current_dir,
filtered_projects,
local_path,
)

click.echo("Available projects:")
for idx, proj in enumerate(filtered_projects, 1):
project_type = (
"Active Project" if proj["id"] == active_project_id else "Submodule"
)
default_marker = " (default)" if idx - 1 == default_project else ""
click.echo(
f"{idx}. {proj['name']} (ID: {proj['id']}) - {project_type}{default_marker}"
)

while True:
prompt = "Enter the number of the project to associate with the chat"
if default_project is not None:
default_project_name = filtered_projects[default_project]["name"]
prompt += f" (default: {default_project + 1} - {default_project_name})"
selection = click.prompt(
prompt,
type=int,
default=default_project + 1 if default_project is not None else None,
)
if 1 <= selection <= len(filtered_projects):
project = filtered_projects[selection - 1]["id"]
break
click.echo("Invalid selection. Please try again.")
return project


def get_default_project(
active_project_id, active_project_name, current_dir, filtered_projects, local_path
):
# Find the project that matches the current directory
default_project = None
for idx, proj in enumerate(filtered_projects):
if proj["id"] == active_project_id:
project_path = os.path.abspath(local_path)
else:
submodule_name = proj["name"].replace(
f"{active_project_name}-SubModule-", ""
)
project_path = os.path.abspath(
os.path.join(local_path, "services", submodule_name)
)
if current_dir.startswith(project_path):
default_project = idx
break
return default_project
4 changes: 4 additions & 0 deletions src/claudesync/cli/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import click

from .category import category
from ..exceptions import ConfigurationError
from ..utils import handle_errors

Expand Down Expand Up @@ -60,3 +61,6 @@ def ls(config):
"""List all configuration values."""
for key, value in config.config.items():
click.echo(f"{key}: {value}")


config.add_command(category)
3 changes: 1 addition & 2 deletions src/claudesync/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .api import api
from .organization import organization
from .project import project
from .sync import ls, sync, schedule
from .sync import ls, schedule
from .config import config
import logging

Expand Down Expand Up @@ -58,7 +58,6 @@ def status(config):
cli.add_command(organization)
cli.add_command(project)
cli.add_command(ls)
cli.add_command(sync)
cli.add_command(schedule)
cli.add_command(config)
cli.add_command(chat)
Expand Down
Loading

0 comments on commit fbf9d1b

Please sign in to comment.