diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 886d86c..550f608 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -43,6 +43,11 @@ jobs: run: | curl -L https://raw.githubusercontent.com/extism/js-pdk/main/install.sh | bash + - name: Install extism-py and uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + curl -Ls https://raw.githubusercontent.com/extism/python-pdk/main/install.sh | bash + - name: Install xtp CLI run: | curl -L https://static.dylibso.com/cli/install.sh -s | bash diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 78a759b..f2fb047 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,6 +43,11 @@ jobs: run: | curl -L https://raw.githubusercontent.com/extism/js-pdk/main/install.sh | bash + - name: Install extism-py and uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + curl -Ls https://raw.githubusercontent.com/extism/python-pdk/main/install.sh | bash + - name: Install xtp CLI run: | curl -L https://static.dylibso.com/cli/install.sh -s | bash diff --git a/servlets/obsidian/plugin/__init__.py b/servlets/obsidian/plugin/__init__.py new file mode 100755 index 0000000..292f8fa --- /dev/null +++ b/servlets/obsidian/plugin/__init__.py @@ -0,0 +1,45 @@ +# THIS FILE WAS GENERATED BY `xtp-python-bindgen`. DO NOT EDIT. + +from typing import Optional, List # noqa: F401 +from datetime import datetime # noqa: F401 +import extism # pyright: ignore +import plugin +import json + +from pdk_types import ( + BlobResourceContents, + CallToolRequest, + CallToolResult, + Content, + ContentType, + ListToolsResult, + Params, + Role, + TextAnnotation, + TextResourceContents, + ToolDescription, +) # noqa: F401 + + +# Imports + +# Exports +# The implementations for these functions is in `plugin.py` + + +# Called when the tool is invoked. +# If you support multiple tools, you must switch on the input.params.name to detect which tool is being called. +@extism.plugin_fn +def call(): + data = json.loads(extism.input_str()) + res = plugin.call(data) + extism.output(res) + + +# Called by mcpx to understand how and why to use this tool. +# Note: Your servlet configs will not be set when this function is called, +# so do not rely on config in this function +@extism.plugin_fn +def describe(): + res = plugin.describe() + extism.output(res) diff --git a/servlets/obsidian/plugin/pdk_imports.py b/servlets/obsidian/plugin/pdk_imports.py new file mode 100755 index 0000000..ef64c1f --- /dev/null +++ b/servlets/obsidian/plugin/pdk_imports.py @@ -0,0 +1,20 @@ +# THIS FILE WAS GENERATED BY `xtp-python-bindgen`. DO NOT EDIT. + +from typing import Optional, List # noqa: F401 +from datetime import datetime # noqa: F401 +import extism # noqa: F401 # pyright: ignore + + +from pdk_types import ( + BlobResourceContents, + CallToolRequest, + CallToolResult, + Content, + ContentType, + ListToolsResult, + Params, + Role, + TextAnnotation, + TextResourceContents, + ToolDescription, +) # noqa: F401 diff --git a/servlets/obsidian/plugin/pdk_types.py b/servlets/obsidian/plugin/pdk_types.py new file mode 100755 index 0000000..652028d --- /dev/null +++ b/servlets/obsidian/plugin/pdk_types.py @@ -0,0 +1,117 @@ +# THIS FILE WAS GENERATED BY `xtp-python-bindgen`. DO NOT EDIT. + +from __future__ import annotations +from enum import Enum # noqa: F401 +from typing import Optional, List # noqa: F401 +from datetime import datetime # noqa: F401 +from dataclasses import dataclass # noqa: F401 + +import extism # noqa: F401 # pyright: ignore + + +@dataclass +class BlobResourceContents(extism.Json): + # A base64-encoded string representing the binary data of the item. + blob: str + + # The MIME type of this resource, if known. + mimeType: str + + # The URI of this resource. + uri: str + + +@dataclass +class CallToolRequest(extism.Json): + method: str + + params: Params + + +@dataclass +class CallToolResult(extism.Json): + content: List[Content] + + # Whether the tool call ended in an error. + # + # If not set, this is assumed to be false (the call was successful). + isError: bool + + +@dataclass +class Content(extism.Json): + annotations: TextAnnotation + + # The base64-encoded image data. + data: str + + # The MIME type of the image. Different providers may support different image types. + mimeType: str + + # The text content of the message. + text: str + + type: ContentType + + +class ContentType(Enum): + Text = "text" + Image = "image" + Resource = "resource" + + +@dataclass +class ListToolsResult(extism.Json): + # The list of ToolDescription objects provided by this servlet. + tools: List[ToolDescription] + + +@dataclass +class Params(extism.Json): + arguments: dict + + name: str + + +class Role(Enum): + Assistant = "assistant" + User = "user" + + +@dataclass +class TextAnnotation(extism.Json): + # Describes who the intended customer of this object or data is. + # + # It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). + audience: List[Role] + + # Describes how important this data is for operating the server. + # + # A value of 1 means "most important," and indicates that the data is + # effectively required, while 0 means "least important," and indicates that + # the data is entirely optional. + priority: float + + +@dataclass +class TextResourceContents(extism.Json): + # The MIME type of this resource, if known. + mimeType: str + + # The text of the item. This must only be set if the item can actually be represented as text (not binary data). + text: str + + # The URI of this resource. + uri: str + + +@dataclass +class ToolDescription(extism.Json): + # A description of the tool + description: str + + # The JSON schema describing the argument input + inputSchema: dict + + # The name of the tool. It should match the plugin / binding name. + name: str diff --git a/servlets/obsidian/plugin/plugin.py b/servlets/obsidian/plugin/plugin.py new file mode 100755 index 0000000..89bce43 --- /dev/null +++ b/servlets/obsidian/plugin/plugin.py @@ -0,0 +1,298 @@ +from typing import Optional, List # noqa: F401 +from datetime import datetime # noqa: F401 +import extism # noqa: F401 # pyright: ignore +from urllib.parse import urlencode + +from pdk_types import ( + BlobResourceContents, + CallToolRequest, + CallToolResult, + Content, + ContentType, + ListToolsResult, + Params, + Role, + TextAnnotation, + TextResourceContents, + ToolDescription, +) # noqa: F401 + + +from typing import List, Optional # noqa: F401 + +class Obsidian: + def __init__(self): + self.host = extism.Config.get_str('OBSIDIAN_API_URL') + api_key = extism.Config.get_str('OBSIDIAN_API_KEY') + self.headers = {"Authorization": f"Bearer {api_key}"} + + def get(self, path): + extism.log(extism.LogLevel.Info, f"get {self.host}{path}") + res = extism.Http.request(f"{self.host}{path}", 'GET', None, self.headers) + extism.log(extism.LogLevel.Info, f"-> {res.status_code}") + resres = res.data_str() + extism.log(extism.LogLevel.Debug, f"-> {resres}") + return resres + + def post(self, path, body = None, extraheaders = {}): + headers = self.headers | extraheaders + extism.log(extism.LogLevel.Info, f"post {self.host}{path}") + res = extism.Http.request(f"{self.host}{path}", 'POST', body, headers) + extism.log(extism.LogLevel.Info, f"-> {res.status_code}") + resres = res.data_str() + extism.log(extism.LogLevel.Debug, f"-> {resres}") + return resres + + def list_files_in_vault(self): + return self.get('/vault/') + + def list_files_in_dir(self, path): + return self.get(f"/vault/{path}/") + + def get_file_contents(self, path): + return self.get(f"/vault/{path}") + + def search(self, query: str, context_length: int = 100): + params = { + 'query': query, + 'contextLength': context_length + } + query_string = urlencode(params) + return self.post(f"/search/simple/?{query_string}") + + def append_content(self, path, content): + return self.post(f"/vault/{path}", content, {'Content-Type': 'text/markdown'}) + + def patch_content(filepath, operation, target_type, target, content): + headers = { + 'Content-Type': 'text/markdown', + 'Operation': operation, + 'Target-Type': target_type, + 'Target': urllib.parse.quote(target), + } + return self.post(f"/vault/{path}", content, headers) + + def complex_search(self, query): + headers = { + 'Content-Type': 'application/vnd.olrapi.jsonlogic+json', + } + query_string = urlencode(params) + return self.post(f"/search/", json.dumps(query), headers) + +def errorReturn(message): + return CallToolResult( + content=[ + Content( + text=message, + mimeType="text/plain", + type=ContentType.Text, + annotations=None, + data=None, + ) + ], + isError=True, + ) + + +# Called when the tool is invoked. +# If you support multiple tools, you must switch on the input.params.name to detect which tool is being called. +def call(input) -> CallToolResult: + try: + fname = input['params']['name'] + except: + raise Exception("params name must be provided") + obsidian = Obsidian() + match fname: + case "list_files_in_vault": + contentText = obsidian.list_files_in_vault() + case "list_files_in_dir": + try: + dirpath = input['params']['arguments']['dirpath'] + except: + return errorReturn("Argument dirpath not provided") + contentText = obsidian.list_files_in_dir(dirpath) + case "get_file_contents": + try: + filepath = input['params']['arguments']['filepath'] + except: + return errorReturn("Argument filepath not provided") + contentText = obsidian.get_file_contents(filepath) + case "simple_search": + try: + query = input['params']['arguments']['query'] + except: + return errorReturn("Argument query not provided") + context_length = input['params']['arguments'].get('context_length') + contentText = obsidian.search(query, context_length) + case "append_content": + try: + filepath = input['params']['arguments']['filepath'] + content = input['params']['arguments']['content'] + except: + return errorReturn("Argument filepath or content not provided") + contentText = obsidian.append_content(filepath, content) + case "patch_content": + try: + filepath = input['params']['arguments']['filepath'] + operation = input['params']['arguments']['operation'] + target_type = input['params']['arguments']['target_type'] + target = input['params']['arguments']['target'] + content = input['params']['arguments']['content'] + except: + return errorReturn("Arguments missing") + contentText = obsidian.patch_content(filepath, operation, target_type, target, content) + case "complex_search": + try: + query = input['params']['arguments']['query'] + except: + return errorReturn("Argument query not provided") + contentText = obsidian.complex_search(query) + case _: + return errorReturn(f"Unknown tool {fname}") + return CallToolResult( + content=[ + Content( + text=contentText, + mimeType="text/plain", + type=ContentType.Text, + annotations=None, + data=None, + ) + ], + isError=False, + ) + + +# Called by mcpx to understand how and why to use this tool. +# Note: Your servlet configs will not be set when this function is called, +# so do not rely on config in this function +def describe() -> ListToolsResult: + return ListToolsResult( + [ + ToolDescription( + name="list_files_in_vault", + description="Lists all files and directories in the root directory of your Obsidian vault.", + inputSchema={ + "type": "object", + }, + ), + ToolDescription( + name="list_files_in_dir", + description="Lists all files and directories that exist in a specific Obsidian directory.", + inputSchema={ + "type": "object", + "properties": { + "dirpath": { + "type": "string", + "description": "Path to list files from (relative to your vault root). Note that empty directories will not be returned." + }, + }, + "required": ["dirpath"] + }, + ), + ToolDescription( + name="get_file_contents", + description="Return the content of a single file in your vault.", + inputSchema={ + "type": "object", + "properties": { + "filepath": { + "type": "string", + "description": "Path to the relevant file (relative to your vault root).", + "format": "path" + }, + }, + "required": ["filepath"] + }, + ), + ToolDescription( + name="simple_search", + description="""Simple search for documents matching a specified text query across all files in the vault. + Use this tool when you want to do a simple text search""", + inputSchema={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Text to a simple search for in the vault." + }, + "context_length": { + "type": "integer", + "description": "How much context to return around the matching string (default: 100)", + "default": 100 + } + }, + "required": ["query"] + }, + ), + ToolDescription( + name="append_content", + description="Append content to a new or existing file in the vault.", + inputSchema={ + "type": "object", + "properties": { + "filepath": { + "type": "string", + "description": "Path to the file (relative to vault root)", + "format": "path" + }, + "content": { + "type": "string", + "description": "Content to append to the file" + } + }, + "required": ["filepath", "content"] + }, + ), + ToolDescription( + name="patch_content", + description="Insert content into an existing note relative to a heading, block reference, or frontmatter field.", + inputSchema={ + "type": "object", + "properties": { + "filepath": { + "type": "string", + "description": "Path to the file (relative to vault root)", + "format": "path" + }, + "operation": { + "type": "string", + "description": "Operation to perform (append, prepend, or replace)", + "enum": ["append", "prepend", "replace"] + }, + "target_type": { + "type": "string", + "description": "Type of target to patch", + "enum": ["heading", "block", "frontmatter"] + }, + "target": { + "type": "string", + "description": "Target identifier (heading path, block reference, or frontmatter field)" + }, + "content": { + "type": "string", + "description": "Content to insert" + } + }, + "required": ["filepath", "operation", "target_type", "target", "content"] + }, + ), + ToolDescription( + name="complex_search", + description="""Complex search for documents using a JsonLogic query. + Supports standard JsonLogic operators plus 'glob' and 'regexp' for pattern matching. Results must be non-falsy. + + Use this tool when you want to do a complex search, e.g. for all documents with certain tags etc.""", + inputSchema={ + "type": "object", + "properties": { + "query": { + "type": "object", + "description": "JsonLogic query object. Example: {\"glob\": [\"*.md\", {\"var\": \"path\"}]} matches all markdown files" + } + }, + "required": ["query"] + }, + ), + ] + ) diff --git a/servlets/obsidian/prepare.sh b/servlets/obsidian/prepare.sh new file mode 100644 index 0000000..546e38f --- /dev/null +++ b/servlets/obsidian/prepare.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Function to check if a command exists +command_exists () { + command -v "$1" >/dev/null 2>&1 +} + +missing_deps=0 + +# Check for uv +if ! (command_exists uv); then + missing_deps=1 + echo "❌ uv is not installed." + echo "" + echo "To install uv, visit the official download page:" + echo "👉 https://docs.astral.sh/uv/getting-started/installation/" + echo "" +fi + +# Exit with a bad exit code if any dependencies are missing +if [ "$missing_deps" -ne 0 ]; then + echo "Install the missing dependencies and ensure they are on your path. Then run this command again." + # TODO: remove sleep when cli bug is fixed + sleep 2 + exit 1 +fi + +# Check for extism-js +if ! command_exists extism-py; then + echo "❌ extism-py is not installed." + echo "" + echo "extism-py is needed to compile the plug-in. You can find the instructions to install it here: https://github.com/extism/python-pdk" + echo "" + echo "Alternatively, you can use an install script." + echo "" + echo "🔹 Mac / Linux:" + echo "curl -L https://raw.githubusercontent.com/extism/python-pdk/main/install.sh | bash" + echo "" + # TODO: remove sleep when cli bug is fixed + sleep 2 + exit 1 +fi + + diff --git a/servlets/obsidian/pyproject.toml b/servlets/obsidian/pyproject.toml new file mode 100755 index 0000000..7065981 --- /dev/null +++ b/servlets/obsidian/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "obsidian" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[tool.uv] +dev-dependencies = [ + "pyright>=1.1.381", + "ruff>=0.6.5", +] diff --git a/servlets/obsidian/pyrightconfig.json b/servlets/obsidian/pyrightconfig.json new file mode 100755 index 0000000..0e952c0 --- /dev/null +++ b/servlets/obsidian/pyrightconfig.json @@ -0,0 +1,5 @@ +{ + "executionEnvironments": [ + {"root": "plugin"} + ] +} diff --git a/servlets/obsidian/uv.lock b/servlets/obsidian/uv.lock new file mode 100644 index 0000000..1815d35 --- /dev/null +++ b/servlets/obsidian/uv.lock @@ -0,0 +1,77 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "obsidian" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pyright", specifier = ">=1.1.381" }, + { name = "ruff", specifier = ">=0.6.5" }, +] + +[[package]] +name = "pyright" +version = "1.1.391" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/05/4ea52a8a45cc28897edb485b4102d37cbfd5fce8445d679cdeb62bfad221/pyright-1.1.391.tar.gz", hash = "sha256:66b2d42cdf5c3cbab05f2f4b76e8bec8aa78e679bfa0b6ad7b923d9e027cadb2", size = 21965 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/89/66f49552fbeb21944c8077d11834b2201514a56fd1b7747ffff9630f1bd9/pyright-1.1.391-py3-none-any.whl", hash = "sha256:54fa186f8b3e8a55a44ebfa842636635688670c6896dcf6cf4a7fc75062f4d15", size = 18579 }, +] + +[[package]] +name = "ruff" +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/5e/683c7ef7a696923223e7d95ca06755d6e2acbc5fd8382b2912a28008137c/ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3", size = 3378522 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/c4/bfdbb8b9c419ff3b52479af8581026eeaac3764946fdb463dec043441b7d/ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6", size = 10535860 }, + { url = "https://files.pythonhosted.org/packages/ef/c5/0aabdc9314b4b6f051168ac45227e2aa8e1c6d82718a547455e40c9c9faa/ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939", size = 10346327 }, + { url = "https://files.pythonhosted.org/packages/1a/78/4843a59e7e7b398d6019cf91ab06502fd95397b99b2b858798fbab9151f5/ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d", size = 9942585 }, + { url = "https://files.pythonhosted.org/packages/91/5a/642ed8f1ba23ffc2dd347697e01eef3c42fad6ac76603be4a8c3a9d6311e/ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13", size = 10797597 }, + { url = "https://files.pythonhosted.org/packages/30/25/2e654bc7226da09a49730a1a2ea6e89f843b362db80b4b2a7a4f948ac986/ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18", size = 10307244 }, + { url = "https://files.pythonhosted.org/packages/c0/2d/a224d56bcd4383583db53c2b8f410ebf1200866984aa6eb9b5a70f04e71f/ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502", size = 11362439 }, + { url = "https://files.pythonhosted.org/packages/82/01/03e2857f9c371b8767d3e909f06a33bbdac880df17f17f93d6f6951c3381/ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d", size = 12078538 }, + { url = "https://files.pythonhosted.org/packages/af/ae/ff7f97b355da16d748ceec50e1604a8215d3659b36b38025a922e0612e9b/ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82", size = 11616172 }, + { url = "https://files.pythonhosted.org/packages/6a/d0/6156d4d1e53ebd17747049afe801c5d7e3014d9b2f398b9236fe36ba4320/ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452", size = 12919886 }, + { url = "https://files.pythonhosted.org/packages/4e/84/affcb30bacb94f6036a128ad5de0e29f543d3f67ee42b490b17d68e44b8a/ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd", size = 11212599 }, + { url = "https://files.pythonhosted.org/packages/60/b9/5694716bdefd8f73df7c0104334156c38fb0f77673d2966a5a1345bab94d/ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20", size = 10784637 }, + { url = "https://files.pythonhosted.org/packages/24/7e/0e8f835103ac7da81c3663eedf79dec8359e9ae9a3b0d704bae50be59176/ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc", size = 10390591 }, + { url = "https://files.pythonhosted.org/packages/27/da/180ec771fc01c004045962ce017ca419a0281f4bfaf867ed0020f555b56e/ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060", size = 10894298 }, + { url = "https://files.pythonhosted.org/packages/6d/f8/29f241742ed3954eb2222314b02db29f531a15cab3238d1295e8657c5f18/ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea", size = 11275965 }, + { url = "https://files.pythonhosted.org/packages/79/e9/5b81dc9afc8a80884405b230b9429efeef76d04caead904bd213f453b973/ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964", size = 8807651 }, + { url = "https://files.pythonhosted.org/packages/ea/67/7291461066007617b59a707887b90e319b6a043c79b4d19979f86b7a20e7/ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9", size = 9625289 }, + { url = "https://files.pythonhosted.org/packages/03/8f/e4fa95288b81233356d9a9dcaed057e5b0adc6399aa8fd0f6d784041c9c3/ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936", size = 9078754 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] diff --git a/servlets/obsidian/xtp.toml b/servlets/obsidian/xtp.toml new file mode 100755 index 0000000..4e71aa1 --- /dev/null +++ b/servlets/obsidian/xtp.toml @@ -0,0 +1,17 @@ +app_id = "app_01je4dgpcyfvgrz8f1ys3pbxas" + +# This is where 'xtp plugin push' expects to find the wasm file after the build script has run. +bin = "plugin.wasm" +extension_point_id = "ext_01je4jj1tteaktf0zd0anm8854" +name = "obsidian" + +[scripts] + + # xtp plugin build runs this script to generate the wasm file + build = "PYTHONPATH=./plugin extism-py -o plugin.wasm plugin/__init__.py" + + # xtp plugin init runs this script to format the plugin code + format = "uv run ruff format plugin/*.py" + + # xtp plugin init runs this script before running the format script + prepare = "bash prepare.sh && uv sync"