@inline-chat/hermes-agent-adapter 0.0.4 → 0.0.5-alpha.1
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.
- package/README.md +14 -8
- package/dist/install.js +74 -74
- package/package.json +10 -9
- package/plugin/inline/adapter.py +21 -23
- package/plugin/inline/cli.py +276 -14
- package/plugin/inline/plugin.yaml +2 -2
- package/plugin/inline/sidecar/index.mjs +68 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inline-chat/hermes-agent-adapter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5-alpha.1",
|
|
4
4
|
"description": "Hermes Agent platform adapter for Inline, with a native Python plugin and bundled Inline realtime sidecar.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -49,11 +49,12 @@
|
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "mkdir -p plugin/inline/sidecar && bun build ./src/install.ts --outdir dist --entry-naming '[name].js' --target=node --format=esm --packages=bundle && bun build ./src/sidecar/index.ts --outdir plugin/inline/sidecar --entry-naming 'index.mjs' --target=node --format=esm --packages=bundle && tsc -p tsconfig.json --emitDeclarationOnly",
|
|
52
|
-
"lint": "bunx oxlint --ignore-path ../.oxlintignore src/install.ts src/sidecar/contract.ts src/sidecar/index.ts tests/adapter-python.test.ts tests/install.test.ts tests/package-artifact.test.ts tests/sidecar-contract.test.ts tests/sidecar-runtime.test.ts vitest.config.ts",
|
|
53
|
-
"pretest": "bun run build
|
|
52
|
+
"lint": "bunx oxlint --ignore-path ../.oxlintignore scripts/release-stage.mjs src/install.ts src/sidecar/contract.ts src/sidecar/index.ts tests/adapter-python.test.ts tests/install.test.ts tests/package-artifact.test.ts tests/sidecar-contract.test.ts tests/sidecar-runtime.test.ts vitest.config.ts",
|
|
53
|
+
"pretest": "bun run build",
|
|
54
54
|
"prepack": "bun run build",
|
|
55
55
|
"prepublishOnly": "bun run check",
|
|
56
|
-
"release:preflight": "
|
|
56
|
+
"release:preflight": "node ./scripts/release-stage.mjs --dry-run",
|
|
57
|
+
"release:stage": "node ./scripts/release-stage.mjs --prepare-only",
|
|
57
58
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
58
59
|
"test": "vitest run --coverage",
|
|
59
60
|
"check": "bun run typecheck && bun run lint && bun run test"
|
|
@@ -63,10 +64,10 @@
|
|
|
63
64
|
"yaml": "2.9.0"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
|
-
"@types/node": "20.
|
|
67
|
-
"@vitest/coverage-v8": "4.
|
|
67
|
+
"@types/node": "20.19.43",
|
|
68
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
68
69
|
"typescript": "5.8.3",
|
|
69
|
-
"vitest": "4.
|
|
70
|
+
"vitest": "4.1.10"
|
|
70
71
|
},
|
|
71
72
|
"publishConfig": {
|
|
72
73
|
"access": "public"
|
|
@@ -75,7 +76,7 @@
|
|
|
75
76
|
"pluginId": "inline",
|
|
76
77
|
"pluginPath": "plugin/inline",
|
|
77
78
|
"minHermesVersion": "0.17.0",
|
|
78
|
-
"testedHermesVersion": "0.
|
|
79
|
-
"testedHermesCommit": "
|
|
79
|
+
"testedHermesVersion": "0.18.2",
|
|
80
|
+
"testedHermesCommit": "9de9c25"
|
|
80
81
|
}
|
|
81
82
|
}
|
package/plugin/inline/adapter.py
CHANGED
|
@@ -17,6 +17,7 @@ import re
|
|
|
17
17
|
import secrets
|
|
18
18
|
import shutil
|
|
19
19
|
import signal
|
|
20
|
+
import socket
|
|
20
21
|
import subprocess
|
|
21
22
|
import sys
|
|
22
23
|
import time
|
|
@@ -78,7 +79,6 @@ _INLINE_COMMAND_LIMIT = 100
|
|
|
78
79
|
_INLINE_COMMAND_DESCRIPTION_LIMIT = 256
|
|
79
80
|
_INLINE_COMMAND_RETRY_RATIO = 0.8
|
|
80
81
|
_INLINE_COMMAND_RE = re.compile(r"^[a-z0-9_]{1,32}$")
|
|
81
|
-
_REPLY_THREAD_AUTO_CREATE_MIN_MESSAGES = 50
|
|
82
82
|
_INLINE_THREADS_COMMAND_DESCRIPTION = "Configure Inline reply-thread routing"
|
|
83
83
|
_INLINE_THREADS_COMMAND_ARGS = "[status|on|off|auto|reset]"
|
|
84
84
|
_INLINE_THREADS_ACTION_PREFIX = "th:"
|
|
@@ -213,6 +213,14 @@ def _normalize_sidecar_port(value: Any) -> int:
|
|
|
213
213
|
return port
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def _available_loopback_port(bind: str) -> int:
|
|
217
|
+
"""Choose an unused loopback port for a short-lived standalone sidecar."""
|
|
218
|
+
family = socket.AF_INET6 if ":" in bind else socket.AF_INET
|
|
219
|
+
with socket.socket(family, socket.SOCK_STREAM) as sock:
|
|
220
|
+
sock.bind((bind, 0))
|
|
221
|
+
return int(sock.getsockname()[1])
|
|
222
|
+
|
|
223
|
+
|
|
216
224
|
def _normalize_positive_float(value: Any, default: float, name: str) -> float:
|
|
217
225
|
if value is None or str(value).strip() == "":
|
|
218
226
|
return default
|
|
@@ -584,15 +592,19 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
584
592
|
supports_code_blocks = True
|
|
585
593
|
splits_long_messages = True
|
|
586
594
|
|
|
587
|
-
def __init__(self, config: PlatformConfig):
|
|
595
|
+
def __init__(self, config: PlatformConfig, *, use_ephemeral_sidecar_port: bool = False):
|
|
588
596
|
super().__init__(config, Platform("inline"))
|
|
589
597
|
extra = config.extra or {}
|
|
590
598
|
|
|
591
599
|
self._token = _config_token(config)
|
|
592
600
|
self._base_url = os.getenv("INLINE_BASE_URL") or extra.get("base_url") or "https://api.inline.chat"
|
|
593
|
-
sidecar_port = extra.get("sidecar_port") if "sidecar_port" in extra else os.getenv("INLINE_SIDECAR_PORT")
|
|
594
|
-
self._sidecar_port = _normalize_sidecar_port(sidecar_port)
|
|
595
601
|
self._sidecar_bind = _normalize_sidecar_bind(extra.get("sidecar_bind") or os.getenv("INLINE_SIDECAR_BIND"))
|
|
602
|
+
sidecar_port = extra.get("sidecar_port") if "sidecar_port" in extra else os.getenv("INLINE_SIDECAR_PORT")
|
|
603
|
+
self._sidecar_port = (
|
|
604
|
+
_available_loopback_port(self._sidecar_bind)
|
|
605
|
+
if use_ephemeral_sidecar_port
|
|
606
|
+
else _normalize_sidecar_port(sidecar_port)
|
|
607
|
+
)
|
|
596
608
|
self._connect_timeout_ms = _normalize_positive_float(
|
|
597
609
|
extra.get("connect_timeout_ms") if "connect_timeout_ms" in extra else os.getenv("INLINE_CONNECT_TIMEOUT_MS"),
|
|
598
610
|
_DEFAULT_CONNECT_TIMEOUT_MS,
|
|
@@ -860,16 +872,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
860
872
|
self._reply_thread_overrides[key] = _reply_thread_mode(value, "auto")
|
|
861
873
|
self._save_reply_thread_overrides()
|
|
862
874
|
|
|
863
|
-
@staticmethod
|
|
864
|
-
def _positive_int(value: Any) -> Optional[int]:
|
|
865
|
-
text = str(value or "").strip()
|
|
866
|
-
if not re.fullmatch(r"[1-9]\d*", text):
|
|
867
|
-
return None
|
|
868
|
-
try:
|
|
869
|
-
return int(text)
|
|
870
|
-
except ValueError:
|
|
871
|
-
return None
|
|
872
|
-
|
|
873
875
|
@staticmethod
|
|
874
876
|
def _has_reply_thread_intent(text: str) -> bool:
|
|
875
877
|
normalized = re.sub(r"\s+", " ", str(text or "").strip())
|
|
@@ -884,7 +886,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
884
886
|
*,
|
|
885
887
|
chat_id: str,
|
|
886
888
|
parent_chat_id: Optional[str],
|
|
887
|
-
msg_id: str,
|
|
888
889
|
text: str,
|
|
889
890
|
) -> bool:
|
|
890
891
|
mode = self._reply_thread_mode_for_chat(chat_id, parent_chat_id)
|
|
@@ -894,8 +895,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
894
895
|
return True
|
|
895
896
|
if self._has_reply_thread_intent(text):
|
|
896
897
|
return True
|
|
897
|
-
|
|
898
|
-
return message_id is not None and message_id >= _REPLY_THREAD_AUTO_CREATE_MIN_MESSAGES
|
|
898
|
+
return False
|
|
899
899
|
|
|
900
900
|
@staticmethod
|
|
901
901
|
def _id_allowed(entries: set[str], value: str) -> bool:
|
|
@@ -978,10 +978,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
978
978
|
if mode == "on":
|
|
979
979
|
behavior = "Top-level replies will start or reuse Inline reply threads."
|
|
980
980
|
elif mode == "auto":
|
|
981
|
-
behavior =
|
|
982
|
-
"Top-level replies stay in the parent chat until "
|
|
983
|
-
f"message #{_REPLY_THREAD_AUTO_CREATE_MIN_MESSAGES} or an explicit thread request."
|
|
984
|
-
)
|
|
981
|
+
behavior = "Top-level replies stay in the parent chat unless a thread is explicitly requested."
|
|
985
982
|
else:
|
|
986
983
|
behavior = "Top-level replies stay in the parent chat."
|
|
987
984
|
first_line = f"Inline reply threads are {mode} for this chat ({scope})."
|
|
@@ -1516,7 +1513,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1516
1513
|
and self._should_create_reply_thread_for_message(
|
|
1517
1514
|
chat_id=chat_id,
|
|
1518
1515
|
parent_chat_id=parent_chat_id,
|
|
1519
|
-
msg_id=msg_id,
|
|
1520
1516
|
text=text,
|
|
1521
1517
|
)
|
|
1522
1518
|
):
|
|
@@ -3568,7 +3564,9 @@ async def _standalone_send(pconfig: PlatformConfig, chat_id: str, message: str,
|
|
|
3568
3564
|
token = _config_token(pconfig)
|
|
3569
3565
|
if not token:
|
|
3570
3566
|
return {"error": "Inline token is required in INLINE_TOKEN, INLINE_BOT_TOKEN, or Hermes Inline config"}
|
|
3571
|
-
|
|
3567
|
+
# The supervised gateway may already own the configured sidecar port. A
|
|
3568
|
+
# one-shot sender launches its own sidecar, so isolate it on a free port.
|
|
3569
|
+
adapter = InlineAdapter(pconfig, use_ephemeral_sidecar_port=True)
|
|
3572
3570
|
ok = await adapter.connect()
|
|
3573
3571
|
if not ok:
|
|
3574
3572
|
return {"error": "failed to connect Inline adapter"}
|
package/plugin/inline/cli.py
CHANGED
|
@@ -7,33 +7,296 @@ the plugin has already been discovered by Hermes.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
+
import json
|
|
10
11
|
import os
|
|
11
12
|
import re
|
|
12
13
|
import shutil
|
|
13
14
|
import subprocess
|
|
15
|
+
import sys
|
|
14
16
|
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
|
|
17
19
|
_SIDECAR_ENTRY = Path(__file__).parent / "sidecar" / "index.mjs"
|
|
18
20
|
_MIN_NODE_MAJOR = 20
|
|
21
|
+
_BOT_USERNAME_RE = re.compile(r"^[A-Za-z0-9_]+bot$", re.IGNORECASE)
|
|
22
|
+
_CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
def gateway_setup() -> None:
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
"""Interactively create or connect an Inline bot.
|
|
27
|
+
|
|
28
|
+
Hermes invokes this from its messaging-platform wizard. Keep credential
|
|
29
|
+
persistence inside Hermes' own config helper so profiles and file
|
|
30
|
+
permissions behave exactly like the built-in Telegram setup.
|
|
31
|
+
"""
|
|
32
|
+
from hermes_cli import gateway as hermes_gateway
|
|
33
|
+
from hermes_cli import setup as hermes_setup
|
|
34
|
+
|
|
35
|
+
hermes_setup.print_header("Inline")
|
|
36
|
+
existing = (
|
|
37
|
+
hermes_gateway.get_env_value("INLINE_TOKEN")
|
|
38
|
+
or hermes_gateway.get_env_value("INLINE_BOT_TOKEN")
|
|
39
|
+
)
|
|
40
|
+
if existing:
|
|
41
|
+
hermes_setup.print_info("Inline is already configured.")
|
|
42
|
+
if not hermes_setup.prompt_yes_no("Reconfigure Inline?", False):
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
hermes_setup.print_info("How would you like to connect Hermes to Inline?")
|
|
46
|
+
print()
|
|
47
|
+
hermes_setup.print_info(" [1] Create a bot in Inline and paste its token")
|
|
48
|
+
hermes_setup.print_info(" Go to Settings → Bots → Create a new bot.")
|
|
49
|
+
hermes_setup.print_info(" https://inline.chat/docs/creating-a-bot")
|
|
50
|
+
print()
|
|
51
|
+
hermes_setup.print_info(" [2] Create a bot with the Inline CLI")
|
|
52
|
+
hermes_setup.print_info(" Install or sign in to the CLI, then create the bot here.")
|
|
53
|
+
print()
|
|
54
|
+
|
|
55
|
+
choice = hermes_setup.prompt("Choice [1/2]", default="1").strip()
|
|
56
|
+
owner_user_id: str | None = None
|
|
57
|
+
token: str | None = None
|
|
58
|
+
if choice == "2":
|
|
59
|
+
token, owner_user_id = _create_bot_with_inline_cli(hermes_setup)
|
|
60
|
+
if not token:
|
|
61
|
+
print()
|
|
62
|
+
hermes_setup.print_info("Falling back to an existing bot token...")
|
|
63
|
+
|
|
64
|
+
if not token:
|
|
65
|
+
token = _prompt_existing_token(hermes_setup)
|
|
66
|
+
if not token:
|
|
67
|
+
hermes_setup.print_warning("No token saved. Inline setup was cancelled.")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
hermes_gateway.save_env_value("INLINE_TOKEN", token)
|
|
71
|
+
hermes_setup.print_success("Inline bot token saved securely by Hermes.")
|
|
72
|
+
|
|
73
|
+
if not owner_user_id:
|
|
74
|
+
owner_user_id = _inline_cli_user_id(shutil.which("inline"))
|
|
75
|
+
_configure_access(hermes_gateway, hermes_setup, owner_user_id)
|
|
76
|
+
|
|
77
|
+
print()
|
|
78
|
+
hermes_setup.print_success("💬 Inline is configured!")
|
|
79
|
+
hermes_setup.print_info("Restart the gateway when prompted, then message your bot in Inline.")
|
|
80
|
+
hermes_setup.print_info("Send /sethome in that chat to use it for cron results and notifications.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _create_bot_with_inline_cli(hermes_setup) -> tuple[str | None, str | None]:
|
|
84
|
+
inline_bin = _find_inline_cli()
|
|
85
|
+
if not inline_bin:
|
|
86
|
+
inline_bin = _install_inline_cli(hermes_setup)
|
|
87
|
+
if not inline_bin:
|
|
88
|
+
hermes_setup.print_warning("Automatic bot creation is unavailable because the Inline CLI could not be installed.")
|
|
89
|
+
hermes_setup.print_info("Install it from https://inline.chat/docs/cli, or use an existing bot token.")
|
|
90
|
+
return None, None
|
|
91
|
+
|
|
92
|
+
owner_user_id = _inline_cli_user_id(inline_bin)
|
|
93
|
+
if not owner_user_id:
|
|
94
|
+
print()
|
|
95
|
+
hermes_setup.print_info("Sign in to Inline to create your Hermes bot.")
|
|
96
|
+
if not hermes_setup.prompt_yes_no("Sign in now?", True):
|
|
97
|
+
return None, None
|
|
98
|
+
login = subprocess.run([inline_bin, "auth", "login"], check=False)
|
|
99
|
+
if login.returncode != 0:
|
|
100
|
+
hermes_setup.print_warning("Inline sign-in did not finish successfully.")
|
|
101
|
+
return None, None
|
|
102
|
+
owner_user_id = _inline_cli_user_id(inline_bin)
|
|
103
|
+
if not owner_user_id:
|
|
104
|
+
hermes_setup.print_warning("Inline sign-in could not be verified.")
|
|
105
|
+
return None, None
|
|
106
|
+
|
|
107
|
+
print()
|
|
108
|
+
name = hermes_setup.prompt("Bot name", default="Hermes").strip()
|
|
109
|
+
if not name:
|
|
110
|
+
return None, owner_user_id
|
|
111
|
+
|
|
112
|
+
while True:
|
|
113
|
+
username = hermes_setup.prompt("Bot username (must end in bot)", default="hermesbot").strip().lstrip("@")
|
|
114
|
+
if not _BOT_USERNAME_RE.fullmatch(username):
|
|
115
|
+
hermes_setup.print_warning("Use letters, numbers, or underscores, and end the username with 'bot'.")
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
payload, error = _run_inline_json(
|
|
119
|
+
inline_bin,
|
|
120
|
+
["bots", "create", "--name", name, "--username", username],
|
|
121
|
+
)
|
|
122
|
+
token = str((payload or {}).get("token") or "").strip()
|
|
123
|
+
if token:
|
|
124
|
+
bot = (payload or {}).get("bot")
|
|
125
|
+
bot_name = bot.get("name") if isinstance(bot, dict) else None
|
|
126
|
+
hermes_setup.print_success(f"Created {bot_name or name} in Inline.")
|
|
127
|
+
return token, owner_user_id
|
|
128
|
+
|
|
129
|
+
hermes_setup.print_warning(error or "Inline could not create the bot.")
|
|
130
|
+
if not hermes_setup.prompt_yes_no("Try a different username?", True):
|
|
131
|
+
return None, owner_user_id
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _install_inline_cli(hermes_setup) -> str | None:
|
|
135
|
+
print()
|
|
136
|
+
hermes_setup.print_info("The Inline CLI is needed to create your bot automatically.")
|
|
137
|
+
if not hermes_setup.prompt_yes_no("Install the Inline CLI now?", True):
|
|
138
|
+
hermes_setup.print_info("Inline CLI installation skipped.")
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
brew_bin = shutil.which("brew") if sys.platform == "darwin" else None
|
|
142
|
+
if brew_bin:
|
|
143
|
+
hermes_setup.print_info("Installing the Inline CLI with Homebrew...")
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
[brew_bin, "install", "--cask", "inline"],
|
|
146
|
+
check=False,
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
curl_bin = shutil.which("curl")
|
|
150
|
+
shell_bin = shutil.which("sh") or "/bin/sh"
|
|
151
|
+
if not curl_bin:
|
|
152
|
+
hermes_setup.print_warning("Automatic installation requires curl.")
|
|
153
|
+
return None
|
|
154
|
+
hermes_setup.print_info("Downloading the official Inline CLI installer...")
|
|
155
|
+
try:
|
|
156
|
+
download = subprocess.run(
|
|
157
|
+
[curl_bin, "-fsSL", _CLI_INSTALL_URL],
|
|
158
|
+
stdout=subprocess.PIPE,
|
|
159
|
+
stderr=subprocess.PIPE,
|
|
160
|
+
timeout=60,
|
|
161
|
+
check=False,
|
|
162
|
+
)
|
|
163
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
164
|
+
hermes_setup.print_warning(f"Could not download the Inline CLI installer: {exc}")
|
|
165
|
+
return None
|
|
166
|
+
if download.returncode != 0 or not download.stdout:
|
|
167
|
+
hermes_setup.print_warning("Could not download the Inline CLI installer.")
|
|
168
|
+
return None
|
|
169
|
+
try:
|
|
170
|
+
result = subprocess.run(
|
|
171
|
+
[shell_bin, "-s"],
|
|
172
|
+
input=download.stdout,
|
|
173
|
+
timeout=180,
|
|
174
|
+
check=False,
|
|
175
|
+
)
|
|
176
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
177
|
+
hermes_setup.print_warning(f"Inline CLI installation failed: {exc}")
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
if result.returncode != 0:
|
|
181
|
+
hermes_setup.print_warning("The Inline CLI installer exited unsuccessfully.")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
inline_bin = _find_inline_cli()
|
|
185
|
+
if not inline_bin:
|
|
186
|
+
hermes_setup.print_warning("The Inline CLI was installed but could not be found on PATH.")
|
|
187
|
+
return None
|
|
188
|
+
try:
|
|
189
|
+
verified = subprocess.run(
|
|
190
|
+
[inline_bin, "--version"],
|
|
191
|
+
stdout=subprocess.PIPE,
|
|
192
|
+
stderr=subprocess.PIPE,
|
|
193
|
+
timeout=10,
|
|
194
|
+
check=False,
|
|
195
|
+
)
|
|
196
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
197
|
+
verified = None
|
|
198
|
+
if verified is None or verified.returncode != 0:
|
|
199
|
+
hermes_setup.print_warning("The installed Inline CLI could not be verified.")
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
hermes_setup.print_success("Inline CLI installed successfully.")
|
|
203
|
+
return inline_bin
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _find_inline_cli() -> str | None:
|
|
207
|
+
discovered = shutil.which("inline")
|
|
208
|
+
if discovered:
|
|
209
|
+
return discovered
|
|
210
|
+
candidates = [
|
|
211
|
+
Path("/opt/homebrew/bin/inline"),
|
|
212
|
+
Path("/usr/local/bin/inline"),
|
|
213
|
+
Path.home() / ".local" / "bin" / "inline",
|
|
214
|
+
]
|
|
215
|
+
for candidate in candidates:
|
|
216
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
217
|
+
return str(candidate)
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _prompt_existing_token(hermes_setup) -> str | None:
|
|
222
|
+
print()
|
|
223
|
+
hermes_setup.print_info("Go to Inline → Settings → Bots → Create a new bot, then copy its token.")
|
|
224
|
+
hermes_setup.print_info("Guide: https://inline.chat/docs/creating-a-bot")
|
|
225
|
+
token = hermes_setup.prompt("Inline bot token", password=True).strip()
|
|
226
|
+
return token or None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _configure_access(hermes_gateway, hermes_setup, owner_user_id: str | None) -> None:
|
|
230
|
+
print()
|
|
231
|
+
hermes_setup.print_info("🔒 Choose who can talk to Hermes.")
|
|
232
|
+
allowed: list[str] = []
|
|
233
|
+
if owner_user_id:
|
|
234
|
+
hermes_setup.print_success(f"Detected your Inline user ID: {owner_user_id}")
|
|
235
|
+
if hermes_setup.prompt_yes_no("Allow this Inline account?", True):
|
|
236
|
+
allowed.append(owner_user_id)
|
|
237
|
+
|
|
238
|
+
extra = hermes_setup.prompt("Additional allowed user IDs (comma-separated, optional)").strip()
|
|
239
|
+
for value in extra.replace(" ", "").split(","):
|
|
240
|
+
if value and value not in allowed:
|
|
241
|
+
allowed.append(value)
|
|
242
|
+
|
|
243
|
+
if allowed:
|
|
244
|
+
value = ",".join(allowed)
|
|
245
|
+
hermes_gateway.save_env_value("INLINE_ALLOW_ALL_USERS", "false")
|
|
246
|
+
hermes_gateway.save_env_value("INLINE_ALLOWED_USERS", value)
|
|
247
|
+
hermes_gateway.save_env_value("INLINE_GROUP_ALLOW_FROM", value)
|
|
248
|
+
hermes_gateway.save_env_value("INLINE_DM_POLICY", "allowlist")
|
|
249
|
+
hermes_gateway.save_env_value("INLINE_GROUP_POLICY", "allowlist")
|
|
250
|
+
hermes_setup.print_success("Only the listed Inline users can invoke Hermes.")
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
if hermes_setup.prompt_yes_no("Allow any Inline user who can reach the bot?", False):
|
|
254
|
+
hermes_gateway.save_env_value("INLINE_ALLOW_ALL_USERS", "true")
|
|
255
|
+
hermes_gateway.save_env_value("INLINE_DM_POLICY", "open")
|
|
256
|
+
hermes_gateway.save_env_value("INLINE_GROUP_POLICY", "open")
|
|
257
|
+
hermes_setup.print_warning("Open access enabled. Any reachable Inline user can invoke Hermes.")
|
|
24
258
|
else:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
259
|
+
hermes_gateway.save_env_value("INLINE_ALLOW_ALL_USERS", "false")
|
|
260
|
+
hermes_gateway.save_env_value("INLINE_DM_POLICY", "disabled")
|
|
261
|
+
hermes_gateway.save_env_value("INLINE_GROUP_POLICY", "disabled")
|
|
262
|
+
hermes_setup.print_warning("Messaging is disabled until you add allowed user IDs and re-run setup.")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _inline_cli_user_id(inline_bin: str | None) -> str | None:
|
|
266
|
+
if not inline_bin:
|
|
267
|
+
return None
|
|
268
|
+
payload, _ = _run_inline_json(inline_bin, ["auth", "me"])
|
|
269
|
+
if not payload:
|
|
270
|
+
return None
|
|
271
|
+
raw = payload.get("id")
|
|
272
|
+
return str(raw).strip() if raw is not None and str(raw).strip() else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _run_inline_json(inline_bin: str, args: list[str]) -> tuple[dict | None, str | None]:
|
|
276
|
+
try:
|
|
277
|
+
result = subprocess.run(
|
|
278
|
+
[inline_bin, "--json", "--compact", *args],
|
|
279
|
+
stdout=subprocess.PIPE,
|
|
280
|
+
stderr=subprocess.PIPE,
|
|
281
|
+
text=True,
|
|
282
|
+
timeout=60,
|
|
283
|
+
check=False,
|
|
284
|
+
)
|
|
285
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
286
|
+
return None, f"Inline CLI failed: {exc}"
|
|
287
|
+
if result.returncode != 0:
|
|
288
|
+
detail = (result.stderr or "").strip().splitlines()
|
|
289
|
+
return None, detail[-1] if detail else "Inline CLI exited unsuccessfully."
|
|
290
|
+
try:
|
|
291
|
+
payload = json.loads(result.stdout)
|
|
292
|
+
except (json.JSONDecodeError, TypeError):
|
|
293
|
+
return None, "Inline CLI returned an unreadable response."
|
|
294
|
+
return payload if isinstance(payload, dict) else None, None
|
|
32
295
|
|
|
33
296
|
|
|
34
297
|
def register_cli(parser: argparse.ArgumentParser) -> None:
|
|
35
298
|
subs = parser.add_subparsers(dest="inline_command", required=False)
|
|
36
|
-
subs.add_parser("setup", help="
|
|
299
|
+
subs.add_parser("setup", help="Configure Inline interactively")
|
|
37
300
|
subs.add_parser("status", help="Show Inline adapter status")
|
|
38
301
|
parser.set_defaults(func=dispatch)
|
|
39
302
|
|
|
@@ -47,13 +310,12 @@ def dispatch(args) -> int:
|
|
|
47
310
|
return 0
|
|
48
311
|
if command == "status":
|
|
49
312
|
configured = _env_token_configured()
|
|
50
|
-
print(f"Inline
|
|
51
|
-
print("Hermes config token support: platforms.inline.token or inline.token")
|
|
313
|
+
print(f"Inline configured: {'yes' if configured else 'no'}")
|
|
52
314
|
print(f"Inline sidecar bundled: {'yes' if _SIDECAR_ENTRY.exists() else 'no'}")
|
|
53
315
|
print(f"Node available: {_node_status()}")
|
|
54
316
|
if not configured:
|
|
55
|
-
print("
|
|
56
|
-
print("
|
|
317
|
+
print("Next: run `hermes inline setup` for guided bot setup.")
|
|
318
|
+
print("Advanced diagnostics: inline-hermes doctor --json")
|
|
57
319
|
return 0
|
|
58
320
|
raise SystemExit(f"unknown inline command: {command}")
|
|
59
321
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: inline-platform
|
|
2
2
|
label: Inline
|
|
3
3
|
kind: platform
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.5-alpha.1
|
|
5
5
|
description: >
|
|
6
6
|
Inline platform adapter for Hermes Agent. The adapter runs as a native
|
|
7
7
|
Hermes Python platform plugin and supervises a local Node sidecar that uses
|
|
@@ -79,7 +79,7 @@ optional_env:
|
|
|
79
79
|
prompt: "Free-response Inline chat ids"
|
|
80
80
|
password: false
|
|
81
81
|
- name: INLINE_REPLY_THREADS
|
|
82
|
-
description: "Reply-thread mode: auto, on, or off. Auto creates child reply threads only for
|
|
82
|
+
description: "Reply-thread mode: auto, on, or off. Auto creates child reply threads only for explicit thread requests (default auto)."
|
|
83
83
|
prompt: "Reply-thread mode (auto/on/off)"
|
|
84
84
|
password: false
|
|
85
85
|
- name: INLINE_CONTEXT_BACKFILL
|