hermes-ryzome-plugin 1.0.0__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.
- hermes_ryzome_plugin-1.0.0/PKG-INFO +13 -0
- hermes_ryzome_plugin-1.0.0/hermes_ryzome_plugin.egg-info/PKG-INFO +13 -0
- hermes_ryzome_plugin-1.0.0/hermes_ryzome_plugin.egg-info/SOURCES.txt +14 -0
- hermes_ryzome_plugin-1.0.0/hermes_ryzome_plugin.egg-info/dependency_links.txt +1 -0
- hermes_ryzome_plugin-1.0.0/hermes_ryzome_plugin.egg-info/entry_points.txt +2 -0
- hermes_ryzome_plugin-1.0.0/hermes_ryzome_plugin.egg-info/top_level.txt +1 -0
- hermes_ryzome_plugin-1.0.0/pyproject.toml +29 -0
- hermes_ryzome_plugin-1.0.0/ryzome_hermes_plugin/__init__.py +36 -0
- hermes_ryzome_plugin-1.0.0/ryzome_hermes_plugin/_runner.js +137 -0
- hermes_ryzome_plugin-1.0.0/ryzome_hermes_plugin/plugin.yaml +17 -0
- hermes_ryzome_plugin-1.0.0/ryzome_hermes_plugin/runtime.py +342 -0
- hermes_ryzome_plugin-1.0.0/ryzome_hermes_plugin/schemas.py +17 -0
- hermes_ryzome_plugin-1.0.0/ryzome_hermes_plugin/tool_manifest.json +1030 -0
- hermes_ryzome_plugin-1.0.0/ryzome_hermes_plugin/tools.py +78 -0
- hermes_ryzome_plugin-1.0.0/setup.cfg +4 -0
- hermes_ryzome_plugin-1.0.0/tests/test_plugin.py +166 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermes-ryzome-plugin
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Ryzome canvas and document tools for Hermes Agent
|
|
5
|
+
Author: 0xPlaygrounds
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/plain
|
|
12
|
+
|
|
13
|
+
Ryzome Hermes plugin for canvas and document tools.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermes-ryzome-plugin
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Ryzome canvas and document tools for Hermes Agent
|
|
5
|
+
Author: 0xPlaygrounds
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/plain
|
|
12
|
+
|
|
13
|
+
Ryzome Hermes plugin for canvas and document tools.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
hermes_ryzome_plugin.egg-info/PKG-INFO
|
|
3
|
+
hermes_ryzome_plugin.egg-info/SOURCES.txt
|
|
4
|
+
hermes_ryzome_plugin.egg-info/dependency_links.txt
|
|
5
|
+
hermes_ryzome_plugin.egg-info/entry_points.txt
|
|
6
|
+
hermes_ryzome_plugin.egg-info/top_level.txt
|
|
7
|
+
ryzome_hermes_plugin/__init__.py
|
|
8
|
+
ryzome_hermes_plugin/_runner.js
|
|
9
|
+
ryzome_hermes_plugin/plugin.yaml
|
|
10
|
+
ryzome_hermes_plugin/runtime.py
|
|
11
|
+
ryzome_hermes_plugin/schemas.py
|
|
12
|
+
ryzome_hermes_plugin/tool_manifest.json
|
|
13
|
+
ryzome_hermes_plugin/tools.py
|
|
14
|
+
tests/test_plugin.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ryzome_hermes_plugin
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hermes-ryzome-plugin"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Ryzome canvas and document tools for Hermes Agent"
|
|
9
|
+
readme = { text = "Ryzome Hermes plugin for canvas and document tools.", content-type = "text/plain" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "0xPlaygrounds" }]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
20
|
+
ryzome = "ryzome_hermes_plugin"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
include-package-data = true
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
include = ["ryzome_hermes_plugin"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
ryzome_hermes_plugin = ["plugin.yaml", "tool_manifest.json", "_runner.js"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .runtime import handle_cli_command, is_configured, setup_cli_parser
|
|
7
|
+
from .schemas import TOOL_SCHEMAS
|
|
8
|
+
from .tools import create_tool_handler
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.0"
|
|
11
|
+
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register(ctx: Any) -> None:
|
|
16
|
+
if not is_configured():
|
|
17
|
+
LOGGER.info(
|
|
18
|
+
"Ryzome Hermes plugin loaded without an API key. Run `hermes ryzome setup` "
|
|
19
|
+
"or set `RYZOME_API_KEY` to enable tools."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
for schema in TOOL_SCHEMAS:
|
|
23
|
+
ctx.register_tool(
|
|
24
|
+
name=schema["name"],
|
|
25
|
+
toolset="ryzome",
|
|
26
|
+
schema=schema,
|
|
27
|
+
handler=create_tool_handler(schema["name"], __version__),
|
|
28
|
+
check_fn=is_configured,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
ctx.register_cli_command(
|
|
32
|
+
name="ryzome",
|
|
33
|
+
help="Configure and inspect the Ryzome Hermes plugin",
|
|
34
|
+
setup_fn=setup_cli_parser,
|
|
35
|
+
handler_fn=handle_cli_command,
|
|
36
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { parseConfig, RyzomeApiError, toolRegistry, } from "@ryzome-ai/ryzome-core";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
// Validates the stdin payload before any tool runs so malformed input produces a
|
|
5
|
+
// clear RunnerFailure instead of failing deep inside a tool.execute call.
|
|
6
|
+
const runnerInputSchema = z.object({
|
|
7
|
+
toolName: z.string(),
|
|
8
|
+
params: z.unknown(),
|
|
9
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
10
|
+
});
|
|
11
|
+
function notConfiguredError() {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
error: {
|
|
15
|
+
name: "ConfigError",
|
|
16
|
+
message: "Ryzome API key not configured. Run `hermes ryzome setup --key <api-key>` or set `RYZOME_API_KEY`.",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function serializeError(error, toolName) {
|
|
21
|
+
if (error instanceof RyzomeApiError) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
toolName,
|
|
25
|
+
error: {
|
|
26
|
+
name: error.name,
|
|
27
|
+
message: error.message,
|
|
28
|
+
stage: error.stage,
|
|
29
|
+
status: error.status,
|
|
30
|
+
retryable: error.retryable,
|
|
31
|
+
body: error.body,
|
|
32
|
+
canvasId: error.canvasId,
|
|
33
|
+
documentId: error.documentId,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (error instanceof Error) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
toolName,
|
|
41
|
+
error: {
|
|
42
|
+
name: error.name,
|
|
43
|
+
message: error.message,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
toolName,
|
|
50
|
+
error: {
|
|
51
|
+
name: "Error",
|
|
52
|
+
message: String(error),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function resolveClientConfig(rawConfig) {
|
|
57
|
+
const resolved = parseConfig(rawConfig ?? {});
|
|
58
|
+
if (!resolved.apiKey) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
apiKey: resolved.apiKey,
|
|
63
|
+
apiUrl: resolved.apiUrl,
|
|
64
|
+
appUrl: resolved.appUrl,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function runTool(input) {
|
|
68
|
+
const tool = toolRegistry.find((candidate) => candidate.name === input.toolName);
|
|
69
|
+
if (!tool) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
toolName: input.toolName,
|
|
73
|
+
error: {
|
|
74
|
+
name: "UnknownToolError",
|
|
75
|
+
message: `Unknown Ryzome tool: ${input.toolName}`,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const clientConfig = resolveClientConfig(input.config);
|
|
80
|
+
if (!clientConfig) {
|
|
81
|
+
return notConfiguredError();
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const result = await tool.execute(input.params, clientConfig);
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
toolName: tool.name,
|
|
88
|
+
content: result.content,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return serializeError(error, tool.name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function readStdin() {
|
|
96
|
+
const chunks = [];
|
|
97
|
+
for await (const chunk of process.stdin) {
|
|
98
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
99
|
+
}
|
|
100
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
101
|
+
}
|
|
102
|
+
async function main() {
|
|
103
|
+
try {
|
|
104
|
+
const rawInput = (await readStdin()).trim();
|
|
105
|
+
if (!rawInput) {
|
|
106
|
+
console.log(JSON.stringify({
|
|
107
|
+
ok: false,
|
|
108
|
+
error: {
|
|
109
|
+
name: "InputError",
|
|
110
|
+
message: "Expected a JSON payload on stdin.",
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const parsed = runnerInputSchema.safeParse(JSON.parse(rawInput));
|
|
116
|
+
if (!parsed.success) {
|
|
117
|
+
console.log(JSON.stringify({
|
|
118
|
+
ok: false,
|
|
119
|
+
error: {
|
|
120
|
+
name: "InputError",
|
|
121
|
+
message: `Invalid runner payload: ${parsed.error.message}`,
|
|
122
|
+
},
|
|
123
|
+
}));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const result = await runTool(parsed.data);
|
|
127
|
+
console.log(JSON.stringify(result));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.log(JSON.stringify(serializeError(error)));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const isEntrypoint = typeof process.argv[1] === "string" &&
|
|
134
|
+
pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
135
|
+
if (isEntrypoint) {
|
|
136
|
+
void main();
|
|
137
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: ryzome
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
description: Ryzome canvas and document tools for Hermes Agent
|
|
4
|
+
author: 0xPlaygrounds
|
|
5
|
+
provides_tools:
|
|
6
|
+
- create_ryzome_document
|
|
7
|
+
- create_ryzome_canvas
|
|
8
|
+
- get_ryzome_document
|
|
9
|
+
- get_ryzome_canvas
|
|
10
|
+
- list_ryzome_documents
|
|
11
|
+
- list_ryzome_canvases
|
|
12
|
+
- create_ryzome_plan
|
|
13
|
+
- create_ryzome_research
|
|
14
|
+
- update_ryzome_document
|
|
15
|
+
- save_ryzome_node_to_library
|
|
16
|
+
- upload_ryzome_image
|
|
17
|
+
provides_hooks: []
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Mapping
|
|
12
|
+
|
|
13
|
+
DEFAULT_RYZOME_API_URL = "https://api.ryzome.ai"
|
|
14
|
+
DEFAULT_RYZOME_APP_URL = "https://ryzome.ai"
|
|
15
|
+
RYZOME_API_KEY_ENV_VARS = (
|
|
16
|
+
"RYZOME_OPENCLAW_API_KEY",
|
|
17
|
+
"RYZOME_API_KEY",
|
|
18
|
+
"PLUGIN_USER_CONFIG_API_KEY",
|
|
19
|
+
)
|
|
20
|
+
ALLOWED_KEYS = {"apiKey", "apiUrl", "appUrl"}
|
|
21
|
+
RUNNER_ENV_VAR = "RYZOME_HERMES_RUNNER"
|
|
22
|
+
CONFIG_PATH_ENV_VAR = "RYZOME_HERMES_CONFIG_PATH"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ResolvedConfig:
|
|
27
|
+
api_key: str | None
|
|
28
|
+
api_url: str
|
|
29
|
+
app_url: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def default_config_path() -> Path:
|
|
33
|
+
override = os.getenv(CONFIG_PATH_ENV_VAR)
|
|
34
|
+
if override:
|
|
35
|
+
return Path(override).expanduser()
|
|
36
|
+
return Path.home() / ".hermes" / "ryzome.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _assert_allowed_keys(config: Mapping[str, Any]) -> None:
|
|
40
|
+
unknown = sorted(set(config.keys()) - ALLOWED_KEYS)
|
|
41
|
+
if unknown:
|
|
42
|
+
joined = ", ".join(unknown)
|
|
43
|
+
raise ValueError(f"ryzome config has unknown keys: {joined}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve_env_placeholders(value: str) -> str:
|
|
47
|
+
resolved = value
|
|
48
|
+
while "${" in resolved:
|
|
49
|
+
start = resolved.find("${")
|
|
50
|
+
end = resolved.find("}", start + 2)
|
|
51
|
+
if end == -1:
|
|
52
|
+
break
|
|
53
|
+
env_name = resolved[start + 2 : end]
|
|
54
|
+
env_value = os.getenv(env_name)
|
|
55
|
+
if not env_value:
|
|
56
|
+
raise ValueError(f"Environment variable {env_name} is not set")
|
|
57
|
+
resolved = f"{resolved[:start]}{env_value}{resolved[end + 1:]}"
|
|
58
|
+
return resolved
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_raw_config(path: Path | None = None) -> dict[str, Any]:
|
|
62
|
+
config_path = path or default_config_path()
|
|
63
|
+
if not config_path.exists():
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
raw = json.loads(config_path.read_text(encoding="utf8"))
|
|
67
|
+
if not isinstance(raw, dict):
|
|
68
|
+
raise ValueError("ryzome config must be a JSON object")
|
|
69
|
+
_assert_allowed_keys(raw)
|
|
70
|
+
return raw
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def save_raw_config(config: Mapping[str, Any], path: Path | None = None) -> Path:
|
|
74
|
+
_assert_allowed_keys(config)
|
|
75
|
+
target = path or default_config_path()
|
|
76
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
target.write_text(json.dumps(dict(config), indent=2) + "\n", encoding="utf8")
|
|
78
|
+
return target
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def resolve_api_key_from_env() -> tuple[str | None, str | None]:
|
|
82
|
+
for env_var in RYZOME_API_KEY_ENV_VARS:
|
|
83
|
+
value = os.getenv(env_var)
|
|
84
|
+
if isinstance(value, str) and value.strip():
|
|
85
|
+
return value.strip(), f"environment ({env_var})"
|
|
86
|
+
return None, None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_config(raw: Mapping[str, Any] | None) -> ResolvedConfig:
|
|
90
|
+
config = dict(raw or {})
|
|
91
|
+
if config:
|
|
92
|
+
_assert_allowed_keys(config)
|
|
93
|
+
|
|
94
|
+
api_key: str | None
|
|
95
|
+
raw_api_key = config.get("apiKey")
|
|
96
|
+
if isinstance(raw_api_key, str) and raw_api_key.strip():
|
|
97
|
+
try:
|
|
98
|
+
api_key = _resolve_env_placeholders(raw_api_key.strip())
|
|
99
|
+
except ValueError:
|
|
100
|
+
api_key = None
|
|
101
|
+
else:
|
|
102
|
+
api_key, _ = resolve_api_key_from_env()
|
|
103
|
+
|
|
104
|
+
raw_api_url = config.get("apiUrl")
|
|
105
|
+
raw_app_url = config.get("appUrl")
|
|
106
|
+
api_url = (
|
|
107
|
+
raw_api_url.strip()
|
|
108
|
+
if isinstance(raw_api_url, str) and raw_api_url.strip()
|
|
109
|
+
else DEFAULT_RYZOME_API_URL
|
|
110
|
+
)
|
|
111
|
+
app_url = (
|
|
112
|
+
raw_app_url.strip()
|
|
113
|
+
if isinstance(raw_app_url, str) and raw_app_url.strip()
|
|
114
|
+
else DEFAULT_RYZOME_APP_URL
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return ResolvedConfig(api_key=api_key, api_url=api_url, app_url=app_url)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resolve_api_key_status(raw: Mapping[str, Any] | None) -> tuple[str | None, str | None]:
|
|
121
|
+
api_key, source = resolve_api_key_from_env()
|
|
122
|
+
if api_key:
|
|
123
|
+
return api_key, source
|
|
124
|
+
|
|
125
|
+
config = dict(raw or {})
|
|
126
|
+
raw_api_key = config.get("apiKey")
|
|
127
|
+
if isinstance(raw_api_key, str) and raw_api_key.strip():
|
|
128
|
+
try:
|
|
129
|
+
return _resolve_env_placeholders(raw_api_key.strip()), "config"
|
|
130
|
+
except ValueError:
|
|
131
|
+
return None, None
|
|
132
|
+
|
|
133
|
+
return None, None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def is_configured() -> bool:
|
|
137
|
+
try:
|
|
138
|
+
raw = load_raw_config()
|
|
139
|
+
return bool(parse_config(raw).api_key)
|
|
140
|
+
except Exception:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _mask_secret(value: str) -> str:
|
|
145
|
+
if len(value) <= 8:
|
|
146
|
+
return "****"
|
|
147
|
+
return f"{value[:4]}...{value[-4:]}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def resolve_runner_command(plugin_version: str) -> list[str]:
|
|
151
|
+
del plugin_version
|
|
152
|
+
|
|
153
|
+
override = os.getenv(RUNNER_ENV_VAR)
|
|
154
|
+
if override:
|
|
155
|
+
return shlex.split(override)
|
|
156
|
+
|
|
157
|
+
bundled_runner = Path(__file__).resolve().parent / "_runner.js"
|
|
158
|
+
if bundled_runner.exists():
|
|
159
|
+
return ["node", str(bundled_runner)]
|
|
160
|
+
|
|
161
|
+
dev_runner = Path(__file__).resolve().parents[1] / "dist" / "runner.js"
|
|
162
|
+
if dev_runner.exists():
|
|
163
|
+
return ["node", str(dev_runner)]
|
|
164
|
+
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
"Could not find a Ryzome Hermes runner. Run "
|
|
167
|
+
"`pnpm --filter @ryzome-ai/hermes-ryzome build` from the repo, or set "
|
|
168
|
+
"RYZOME_HERMES_RUNNER to a command that runs the runner."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def run_node_tool(tool_name: str, args: Mapping[str, Any], plugin_version: str) -> dict[str, Any]:
|
|
173
|
+
raw_config = load_raw_config()
|
|
174
|
+
resolved = parse_config(raw_config)
|
|
175
|
+
if not resolved.api_key:
|
|
176
|
+
return {
|
|
177
|
+
"ok": False,
|
|
178
|
+
"error": {
|
|
179
|
+
"name": "ConfigError",
|
|
180
|
+
"message": (
|
|
181
|
+
"Ryzome API key not configured. Run `hermes ryzome setup --key <api-key>` "
|
|
182
|
+
"or set `RYZOME_API_KEY`."
|
|
183
|
+
),
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
payload = {
|
|
188
|
+
"toolName": tool_name,
|
|
189
|
+
"params": dict(args),
|
|
190
|
+
"config": {
|
|
191
|
+
"apiKey": resolved.api_key,
|
|
192
|
+
"apiUrl": resolved.api_url,
|
|
193
|
+
"appUrl": resolved.app_url,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
command = resolve_runner_command(plugin_version)
|
|
198
|
+
completed = subprocess.run(
|
|
199
|
+
command,
|
|
200
|
+
input=json.dumps(payload),
|
|
201
|
+
capture_output=True,
|
|
202
|
+
text=True,
|
|
203
|
+
check=False,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
stdout = completed.stdout.strip()
|
|
207
|
+
stderr = completed.stderr.strip()
|
|
208
|
+
if not stdout:
|
|
209
|
+
return {
|
|
210
|
+
"ok": False,
|
|
211
|
+
"error": {
|
|
212
|
+
"name": "RunnerError",
|
|
213
|
+
"message": stderr or "Ryzome Hermes runner produced no output.",
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
result = json.loads(stdout)
|
|
219
|
+
except json.JSONDecodeError:
|
|
220
|
+
return {
|
|
221
|
+
"ok": False,
|
|
222
|
+
"error": {
|
|
223
|
+
"name": "RunnerError",
|
|
224
|
+
"message": "Ryzome Hermes runner returned invalid JSON.",
|
|
225
|
+
"stdout": stdout,
|
|
226
|
+
"stderr": stderr,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if completed.returncode != 0 and result.get("ok") is not False:
|
|
231
|
+
return {
|
|
232
|
+
"ok": False,
|
|
233
|
+
"error": {
|
|
234
|
+
"name": "RunnerError",
|
|
235
|
+
"message": stderr or "Ryzome Hermes runner exited with a non-zero status.",
|
|
236
|
+
"stdout": stdout,
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def setup_cli_parser(subparser: argparse.ArgumentParser) -> None:
|
|
244
|
+
commands = subparser.add_subparsers(dest="ryzome_command")
|
|
245
|
+
|
|
246
|
+
setup = commands.add_parser("setup", help="Configure the Ryzome plugin")
|
|
247
|
+
setup.add_argument("--key", dest="key", help="Ryzome API key")
|
|
248
|
+
setup.add_argument("--api-url", dest="api_url", help="Ryzome API URL")
|
|
249
|
+
setup.add_argument("--app-url", dest="app_url", help="Ryzome App URL")
|
|
250
|
+
|
|
251
|
+
commands.add_parser("status", help="Show the current Ryzome plugin status")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _prompt_with_default(prompt: str, default: str | None = None) -> str:
|
|
255
|
+
suffix = f" [{default}]" if default else ""
|
|
256
|
+
response = input(f"{prompt}{suffix}: ").strip()
|
|
257
|
+
return response or (default or "")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _handle_setup(args: argparse.Namespace) -> None:
|
|
261
|
+
non_interactive = bool(args.key)
|
|
262
|
+
if not non_interactive and not sys.stdin.isatty():
|
|
263
|
+
print(
|
|
264
|
+
"Error: No TTY available for interactive prompts. Pass the key directly: "
|
|
265
|
+
"hermes ryzome setup --key <api-key>"
|
|
266
|
+
)
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
current = load_raw_config()
|
|
270
|
+
existing = parse_config(current)
|
|
271
|
+
|
|
272
|
+
api_key = args.key.strip() if isinstance(args.key, str) and args.key.strip() else ""
|
|
273
|
+
if not api_key and not non_interactive:
|
|
274
|
+
api_key = _prompt_with_default("Ryzome API key")
|
|
275
|
+
|
|
276
|
+
if not api_key:
|
|
277
|
+
print("No key provided. Ryzome remains unconfigured.")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
api_url = ""
|
|
281
|
+
app_url = ""
|
|
282
|
+
if isinstance(args.api_url, str) and args.api_url.strip():
|
|
283
|
+
api_url = args.api_url.strip()
|
|
284
|
+
elif not non_interactive:
|
|
285
|
+
api_url = _prompt_with_default("API URL", existing.api_url)
|
|
286
|
+
|
|
287
|
+
if isinstance(args.app_url, str) and args.app_url.strip():
|
|
288
|
+
app_url = args.app_url.strip()
|
|
289
|
+
elif not non_interactive:
|
|
290
|
+
app_url = _prompt_with_default("App URL", existing.app_url)
|
|
291
|
+
|
|
292
|
+
next_config = dict(current)
|
|
293
|
+
next_config["apiKey"] = api_key
|
|
294
|
+
if api_url:
|
|
295
|
+
next_config["apiUrl"] = api_url
|
|
296
|
+
if app_url:
|
|
297
|
+
next_config["appUrl"] = app_url
|
|
298
|
+
|
|
299
|
+
config_path = save_raw_config(next_config)
|
|
300
|
+
resolved = parse_config(next_config)
|
|
301
|
+
|
|
302
|
+
print("Ryzome configured.")
|
|
303
|
+
print(f"Config: {config_path}")
|
|
304
|
+
print(f"Key: {_mask_secret(api_key)}")
|
|
305
|
+
print(f"API: {resolved.api_url}")
|
|
306
|
+
print(f"App: {resolved.app_url}")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _handle_status() -> None:
|
|
310
|
+
config_path = default_config_path()
|
|
311
|
+
try:
|
|
312
|
+
raw = load_raw_config(config_path)
|
|
313
|
+
resolved = parse_config(raw)
|
|
314
|
+
api_key, source = resolve_api_key_status(raw)
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
print(f"Ryzome configuration error: {exc}")
|
|
317
|
+
print(f"Config path: {config_path}")
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
if not api_key:
|
|
321
|
+
print("Ryzome is not configured.")
|
|
322
|
+
print("Run `hermes ryzome setup` or set `RYZOME_API_KEY`.")
|
|
323
|
+
print(f"Config path: {config_path}")
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
print(f"Ryzome is configured ({source}).")
|
|
327
|
+
print(f"Config path: {config_path}")
|
|
328
|
+
print(f"Key: {_mask_secret(api_key)}")
|
|
329
|
+
print(f"API: {resolved.api_url}")
|
|
330
|
+
print(f"App: {resolved.app_url}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def handle_cli_command(args: argparse.Namespace) -> None:
|
|
334
|
+
command = getattr(args, "ryzome_command", None)
|
|
335
|
+
if command == "setup":
|
|
336
|
+
_handle_setup(args)
|
|
337
|
+
return
|
|
338
|
+
if command == "status":
|
|
339
|
+
_handle_status()
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
print("Usage: hermes ryzome <setup|status>")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
MANIFEST_PATH = Path(__file__).with_name("tool_manifest.json")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_tool_schemas() -> list[dict[str, Any]]:
|
|
11
|
+
raw = json.loads(MANIFEST_PATH.read_text(encoding="utf8"))
|
|
12
|
+
if not isinstance(raw, list):
|
|
13
|
+
raise ValueError("tool manifest must be a JSON array")
|
|
14
|
+
return raw
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
TOOL_SCHEMAS = load_tool_schemas()
|