Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions libs/hyperpocket/hyperpocket/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from hyperpocket.cli.eject import eject
from hyperpocket.cli.auth_token import create_token_auth_template
from hyperpocket.cli.auth_oauth2 import create_oauth2_auth_template
from hyperpocket.cli.tool_create import create_tool_template, build_tool
from hyperpocket.cli.tool_create import create_tool_template, sync_tool_schema
from hyperpocket.cli.tool_export import export_tool


Expand All @@ -23,7 +23,7 @@ def devtool():
devtool.add_command(create_token_auth_template)
devtool.add_command(create_oauth2_auth_template)
devtool.add_command(create_tool_template)
devtool.add_command(build_tool)
devtool.add_command(sync_tool_schema)
devtool.add_command(export_tool)

cli.add_command(pull)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def {{ tool_name }}(req: {{ capitalized_tool_name }}Request):

return f"successfully deleted calendar events {req.event_id}"
"""
return
return ""


def main():
Expand Down
128 changes: 76 additions & 52 deletions libs/hyperpocket/hyperpocket/cli/tool_create.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import os
import json
import sys
import shutil
import subprocess
from pathlib import Path
import importlib

import click

from hyperpocket.cli.codegen.tool import get_tool_main_template
from hyperpocket.util.flatten_json_schema import flatten_json_schema


@click.command()
Expand All @@ -19,22 +23,16 @@ def create_tool_template(tool_name, language="python"):
"tool_name must be lowercase and contain only letters and underscores"
)

tool_directory_name = tool_name.replace("_", "-")
capitalized_tool_name = "".join(
[word.capitalize() for word in tool_name.split("_")]
)

# Create tool directory
print(f"Generating tool package directory for {tool_name}")
cwd = Path.cwd()
tool_path = cwd / tool_directory_name
tool_path = cwd / tool_name
tool_path.mkdir(parents=True, exist_ok=True)

# Create tool module directory
print(f"Generating tool module directory for {tool_name}")
tool_module_path = tool_path / tool_name
tool_module_path.mkdir(parents=True, exist_ok=True)

# Create uv or poetry init
print(f"Generating pyproject.toml file for {tool_name}")
if shutil.which("uv"):
Expand All @@ -48,7 +46,7 @@ def create_tool_template(tool_name, language="python"):
raise ValueError("uv or poetry must be installed to generate tool project")

# Create __init__.py file
init_file = tool_module_path / "__init__.py"
init_file = tool_path / "__init__.py"
if not init_file.exists():
init_file.write_text(f"""from .__main__ import main

Expand All @@ -57,27 +55,42 @@ def create_tool_template(tool_name, language="python"):

# Create __main__.py file
print(f"Generating tool main file for {tool_name}")
main_file = tool_module_path / "__main__.py"
main_file = tool_path / "__main__.py"
if not main_file.exists():
main_content = get_tool_main_template().render(
capitalized_tool_name=capitalized_tool_name, tool_name=tool_name
)
main_file.write_text(main_content)

# Create config.toml
print(f"Generating config.toml file for {tool_name}")
config_file = tool_path / "config.toml"
if not config_file.exists():
config_file.write_text(f'''name = "{tool_name}"
description = ""
language = "{language}"

[auth]
auth_provider = ""
auth_handler = ""
scopes = []
''')

# Create pocket.json
print(f"Generating pocket.json file for {tool_name}")
pocket_dict = {
"tool": {
"name": tool_name,
"description": "",
"inputSchema": {
"properties": {},
"required": [],
"title": f"{capitalized_tool_name}Request",
"type": "object"
}
},
"language": language,
"auth": {
"auth_provider": "",
"auth_handler": "",
"scopes": [],
},
"variables": {},
"entrypoint": {
"build": "pip install .",
"run": "python __main__.py"
}
}
pocket_json = json.dumps(pocket_dict, indent=2)
pocket_file = tool_path / "pocket.json"
pocket_file.write_text(pocket_json)

# Create .gitignore
print(f"Generating .gitignore file for {tool_name}")
gitignore_file = tool_path / ".gitignore"
Expand All @@ -89,38 +102,49 @@ def create_tool_template(tool_name, language="python"):
if not readme_file.exists():
readme_file.write_text(f"# {tool_name}\n\n")

# Create schema.json
print(f"Generating schema.json file for {tool_name}")
schema_file = tool_path / "schema.json"
schema_file.touch()


@click.command()
@click.argument("tool_path", type=str, required=False)
def build_tool(tool_path):
"""Build the tool at the specified path or current directory."""
def sync_tool_schema(tool_path):
"""Sync the schema of the tool at the main.py file from specified path or current directory."""

cwd = Path.cwd()

# Determine the tool directory
if tool_path is None:
if not (cwd / "config.toml").exists():
raise ValueError("Current working directory must be a tool directory")
else:
potential_path = Path(tool_path)
if (cwd / potential_path).exists():
cwd = cwd / potential_path
elif potential_path.exists():
cwd = potential_path

working_dir = cwd / tool_path
if os.path.isabs(tool_path):
working_dir = Path(tool_path)

pocket_file = working_dir / "pocket.json"
if not pocket_file.exists():
raise ValueError("pocket.json file does not exist")

with open(pocket_file, "r", encoding="utf-8") as f:
pocket_dict = json.load(f)

model_name = pocket_dict["tool"]["inputSchema"]["title"]

model_object = import_class_from_file(working_dir / "__main__.py", model_name)

schema_json = model_object.model_json_schema()
flatten_schema_json = flatten_json_schema(schema_json)

pocket_dict["tool"]["inputSchema"]["properties"] = flatten_schema_json["properties"]
pocket_dict["tool"]["inputSchema"]["required"] = flatten_schema_json["required"]

pocket_json = json.dumps(pocket_dict, indent=2)
pocket_file.write_text(pocket_json)

def import_class_from_file(file_path: Path, class_name: str):
"""Import a class from a Python file using its path and class name."""
spec = importlib.util.spec_from_file_location(class_name, file_path)

if spec and spec.loader:
module = importlib.util.module_from_spec(spec)
sys.modules[class_name] = module
spec.loader.exec_module(module)

if hasattr(module, class_name):
return getattr(module, class_name)
else:
raise ValueError(f"Tool path '{tool_path}' does not exist")

# Build the tool
print(f"Building tool in {cwd}")
if shutil.which("uv"):
subprocess.run(["uv", "build"], cwd=cwd, check=True)
os.remove(cwd / "dist/.gitignore")
elif shutil.which("poetry"):
subprocess.run(["poetry", "build"], cwd=cwd, check=True)
raise AttributeError(f"Class '{class_name}' not found in {file_path}")
else:
raise ValueError("Tool must be a poetry or uv project")
raise ImportError(f"Could not load module from {file_path}")
12 changes: 12 additions & 0 deletions libs/hyperpocket/hyperpocket/pocket_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
from hyperpocket.tool_like import ToolLike


def print_banner():
banner = """
______ __ ______ _____
___ / / /____ ____________________________________________ /________ /_
__ /_/ /__ / / /__ __ \ _ \_ ___/__ __ \ __ \ ___/_ //_/ _ \ __/
_ __ / _ /_/ /__ /_/ / __/ / __ /_/ / /_/ / /__ _ ,< / __/ /_
/_/ /_/ _\__, / _ .___/\___//_/ _ .___/\____/\___/ /_/|_| \___/\__/
/____/ /_/ /_/
"""
print(banner)

class Pocket(object):
server: PocketServer
core: PocketCore
Expand All @@ -21,6 +32,7 @@ def __init__(
use_profile: bool = False,
):
try:
print_banner() # Print the banner when Pocket is initialized
self._uid = str(uuid.uuid4())
self.use_profile = use_profile
self.server = PocketServer.get_instance_and_refcnt_up(self._uid)
Expand Down
Loading