@inline-chat/hermes-agent-adapter 0.0.5-alpha.3 → 0.0.5-alpha.5
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 +21 -3
- package/package.json +1 -1
- package/plugin/inline/adapter.py +392 -12
- package/plugin/inline/plugin.yaml +1 -1
- package/plugin/inline/sidecar/index.mjs +21 -0
package/README.md
CHANGED
|
@@ -26,12 +26,12 @@ Supported:
|
|
|
26
26
|
- Supervised loopback Node sidecar using the Inline realtime SDK.
|
|
27
27
|
- Realtime inbound messages, catch-up, replies to bot messages, and action callbacks.
|
|
28
28
|
- Outbound text, Markdown parsing, opt-in edit-message streaming, long-message splitting, edits, deletes, typing, and presence.
|
|
29
|
-
- Inline reply-thread routing, explicit-request auto mode, `/threads` controls, parent chat metadata, parent/thread prompt fallback, and thread-specific skill bindings.
|
|
29
|
+
- Inline reply-thread routing, explicit-request auto mode, `/threads` controls, explicit `/follow` and `/unfollow` dialog relevance controls, parent chat metadata, parent/thread prompt fallback, and thread-specific skill bindings.
|
|
30
30
|
- Native Hermes `inline` tool for current-chat/thread reads, bounded history and search, exact message lookup, editing/deleting bot-owned messages, reactions, pin/unpin/list pins, reply-thread creation, and avatar presence/status.
|
|
31
31
|
- Per-turn Inline sender/chat/thread IDs, selective reply/thread/observed context, and parent-thread context, with prompt guidance for sender mentions and current chat/thread Markdown links.
|
|
32
32
|
- OpenClaw-style entity summaries for live turns and tool-fetched history, including mentions, text links, thread links, thread-title links, code/pre blocks, bot commands, and group mentions as untrusted Hermes context.
|
|
33
33
|
- DM and group policies, user allowlists, group sender allowlists, mention requirements, strict mention mode, allowed chats, and free-response chats.
|
|
34
|
-
- Native Inline `/` command-menu sync for Hermes slash commands, including `/threads
|
|
34
|
+
- Native Inline `/` command-menu sync for Hermes slash commands, including `/threads`, `/follow`, `/unfollow`, `/inline_update`, and `/update`; typed slash commands continue to work even if menu sync is disabled or rejected.
|
|
35
35
|
- Inline-native buttons for clarify prompts, command approvals, slash confirmations, and model selection.
|
|
36
36
|
- Outbound local photo, video, voice, and document uploads with configurable size caps.
|
|
37
37
|
- Inbound photo, video, voice, and document summaries, with URL-backed media cached locally for Hermes when available.
|
|
@@ -319,12 +319,30 @@ model-picker callbacks. The stricter callback gate prevents group-visible
|
|
|
319
319
|
buttons from becoming a bypass when message intake is otherwise `open`.
|
|
320
320
|
|
|
321
321
|
On gateway startup, the adapter derives the Inline `/` menu from Hermes'
|
|
322
|
-
|
|
322
|
+
declarative local command registry plus Hermes' central slash-command registry,
|
|
323
|
+
normalizes names to Inline Bot API constraints
|
|
323
324
|
(`^[a-z0-9_]+$`, max 32 characters), and calls `setMyCommands`. If Inline
|
|
324
325
|
rejects the full list with `BOT_COMMANDS_TOO_MUCH`, the adapter retries with a
|
|
325
326
|
smaller prefix. Menu sync failures are logged as warnings and do not prevent
|
|
326
327
|
message transport; `/commands` remains the full fallback list.
|
|
327
328
|
|
|
329
|
+
Run `/inline_update` to install the newest adapter from the plugin's current
|
|
330
|
+
npm release channel. Stable installs continue following `latest`, while
|
|
331
|
+
prerelease installs continue following their existing channel such as `alpha`
|
|
332
|
+
or `beta`. Before changing files, the command checks the selected release's
|
|
333
|
+
minimum Hermes version against the running agent and refuses incompatible or
|
|
334
|
+
unverifiable updates. The update runs in the background without exposing the
|
|
335
|
+
Inline token to npm, then asks you to run `/restart` so Hermes loads the
|
|
336
|
+
installed version. Development symlink installs are left untouched.
|
|
337
|
+
Unexpected precheck or installer failures write bounded, credential-redacted
|
|
338
|
+
diagnostics to the standard Hermes log stream under the `[inline-update]`
|
|
339
|
+
marker, so `hermes logs` and Hermes debug reports can surface the root cause.
|
|
340
|
+
|
|
341
|
+
Run `/follow` in an Inline DM, group, or reply thread to explicitly opt into
|
|
342
|
+
eligible unmentioned activity waking Hermes. Run `/unfollow` to explicitly opt
|
|
343
|
+
out; server auto-follow heuristics will not turn following back on, while
|
|
344
|
+
mentions and replies retain their normal relevance behavior.
|
|
345
|
+
|
|
328
346
|
The plugin id is `inline`, which is intentionally the same id an eventual
|
|
329
347
|
bundled Hermes adapter should use.
|
|
330
348
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inline-chat/hermes-agent-adapter",
|
|
3
|
-
"version": "0.0.5-alpha.
|
|
3
|
+
"version": "0.0.5-alpha.5",
|
|
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",
|
package/plugin/inline/adapter.py
CHANGED
|
@@ -20,11 +20,12 @@ import signal
|
|
|
20
20
|
import socket
|
|
21
21
|
import subprocess
|
|
22
22
|
import sys
|
|
23
|
+
import threading
|
|
23
24
|
import time
|
|
24
25
|
from collections import OrderedDict
|
|
25
26
|
from datetime import datetime, timezone
|
|
26
27
|
from pathlib import Path
|
|
27
|
-
from typing import Any, Dict, List, Optional
|
|
28
|
+
from typing import Any, Callable, Dict, List, NamedTuple, Optional
|
|
28
29
|
from urllib.parse import quote, urlparse
|
|
29
30
|
|
|
30
31
|
try:
|
|
@@ -81,12 +82,19 @@ _INLINE_COMMAND_RETRY_RATIO = 0.8
|
|
|
81
82
|
_INLINE_COMMAND_RE = re.compile(r"^[a-z0-9_]{1,32}$")
|
|
82
83
|
_INLINE_THREADS_COMMAND_DESCRIPTION = "Configure Inline reply-thread routing"
|
|
83
84
|
_INLINE_THREADS_COMMAND_ARGS = "[status|on|off|auto|reset]"
|
|
85
|
+
_INLINE_FOLLOW_COMMAND_DESCRIPTION = "Explicitly follow this Inline chat or thread"
|
|
86
|
+
_INLINE_UNFOLLOW_COMMAND_DESCRIPTION = "Explicitly unfollow this Inline chat or thread"
|
|
87
|
+
_INLINE_UPDATE_COMMAND_DESCRIPTION = "Update the Inline Hermes plugin"
|
|
88
|
+
_INLINE_UPDATE_PACKAGE_NAME = "@inline-chat/hermes-agent-adapter"
|
|
89
|
+
_INLINE_UPDATE_PRECHECK_TIMEOUT_SECONDS = 30
|
|
90
|
+
_INLINE_UPDATE_TIMEOUT_SECONDS = 5 * 60
|
|
91
|
+
_INLINE_UPDATE_LOG_MAX_LINES = 80
|
|
92
|
+
_INLINE_UPDATE_LOG_MAX_CHARS = 8_000
|
|
93
|
+
_INLINE_UPDATE_LOCK = threading.Lock()
|
|
84
94
|
_INLINE_THREADS_ACTION_PREFIX = "th:"
|
|
85
95
|
_INLINE_THREADS_ACTION_TTL_SECONDS = 15 * 60
|
|
86
|
-
_INLINE_LOCAL_COMMANDS = (
|
|
87
|
-
("threads", _INLINE_THREADS_COMMAND_DESCRIPTION),
|
|
88
|
-
)
|
|
89
96
|
_INLINE_THREAD_COMMAND_RE = re.compile(r"^/(?:thread|threads)(?:@[A-Za-z0-9_]+)?(?:\s+(.*))?$", re.IGNORECASE)
|
|
97
|
+
_INLINE_FOLLOW_COMMAND_RE = re.compile(r"^/(follow|unfollow)(?:@[A-Za-z0-9_]+)?(?:\s+(.*))?$", re.IGNORECASE)
|
|
90
98
|
_INLINE_REPLY_THREAD_NEGATION_RE = re.compile(
|
|
91
99
|
r"\b(?:do\s+not|don't|dont|please\s+don't|please\s+dont|no\s+need\s+to)\s+"
|
|
92
100
|
r"(?:create|start|open|make|use|move|take|reply|respond|answer|send|thread)\b[^.!?\n]*\bthread\b|"
|
|
@@ -109,6 +117,15 @@ _INLINE_REPLY_THREAD_INTENT_RE = re.compile(
|
|
|
109
117
|
_INLINE_SETTINGS_VERSION = 1
|
|
110
118
|
_INLINE_ENTITY_LIMIT = 12
|
|
111
119
|
_INLINE_ENTITY_TEXT_LIMIT = 120
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class _InlineCommandSpec(NamedTuple):
|
|
123
|
+
name: str
|
|
124
|
+
handler: Callable[[str], Any]
|
|
125
|
+
description: str
|
|
126
|
+
args_hint: str = ""
|
|
127
|
+
|
|
128
|
+
|
|
112
129
|
_INLINE_ENTITY_TYPE_NAMES = {
|
|
113
130
|
1: "mention",
|
|
114
131
|
2: "url",
|
|
@@ -385,6 +402,22 @@ def _normalize_inline_command_name(raw: str) -> str:
|
|
|
385
402
|
return name.strip("_")
|
|
386
403
|
|
|
387
404
|
|
|
405
|
+
def _normalize_inline_plugin_command_text(text: str) -> str:
|
|
406
|
+
match = re.match(r"^/([a-z0-9_]+)(@[A-Za-z0-9_]+)?(\s.*)?$", str(text or ""), re.IGNORECASE | re.DOTALL)
|
|
407
|
+
if not match or "_" not in match.group(1):
|
|
408
|
+
return text
|
|
409
|
+
command = match.group(1).lower().replace("_", "-")
|
|
410
|
+
try:
|
|
411
|
+
from hermes_cli.plugins import get_plugin_commands
|
|
412
|
+
if command not in (get_plugin_commands() or {}):
|
|
413
|
+
return text
|
|
414
|
+
except Exception:
|
|
415
|
+
return text
|
|
416
|
+
# Inline menus require underscores, while Hermes registers plugin commands
|
|
417
|
+
# with hyphens. Normalize before gateway access checks as well as dispatch.
|
|
418
|
+
return f"/{command}{match.group(2) or ''}{match.group(3) or ''}"
|
|
419
|
+
|
|
420
|
+
|
|
388
421
|
def _normalize_inline_command_description(raw: str) -> str:
|
|
389
422
|
description = strip_markdown(str(raw or "")).strip()
|
|
390
423
|
description = re.sub(r"\s+", " ", description)
|
|
@@ -396,7 +429,11 @@ def _normalize_inline_command_description(raw: str) -> str:
|
|
|
396
429
|
def _inline_menu_commands(max_commands: int = _INLINE_COMMAND_LIMIT) -> tuple[List[Dict[str, Any]], int]:
|
|
397
430
|
from hermes_cli.commands import telegram_menu_commands
|
|
398
431
|
|
|
399
|
-
|
|
432
|
+
command_specs = _inline_command_specs()
|
|
433
|
+
local_commands = [
|
|
434
|
+
(spec.name, spec.description)
|
|
435
|
+
for spec in command_specs[:max(0, max_commands)]
|
|
436
|
+
]
|
|
400
437
|
remaining = max(0, max_commands - len(local_commands))
|
|
401
438
|
menu_commands, hidden_count = telegram_menu_commands(max_commands=remaining)
|
|
402
439
|
commands: List[Dict[str, Any]] = []
|
|
@@ -418,7 +455,7 @@ def _inline_menu_commands(max_commands: int = _INLINE_COMMAND_LIMIT) -> tuple[Li
|
|
|
418
455
|
"sort_order": index,
|
|
419
456
|
})
|
|
420
457
|
|
|
421
|
-
hidden_local = max(0, len(
|
|
458
|
+
hidden_local = max(0, len(command_specs) - len(local_commands))
|
|
422
459
|
return commands, hidden_count + hidden_local + skipped
|
|
423
460
|
|
|
424
461
|
|
|
@@ -1081,6 +1118,54 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1081
1118
|
)
|
|
1082
1119
|
return True
|
|
1083
1120
|
|
|
1121
|
+
async def _handle_follow_command(
|
|
1122
|
+
self,
|
|
1123
|
+
*,
|
|
1124
|
+
chat_id: str,
|
|
1125
|
+
msg_id: str,
|
|
1126
|
+
from_id: str,
|
|
1127
|
+
text: str,
|
|
1128
|
+
chat_type: str,
|
|
1129
|
+
thread_id: Optional[str],
|
|
1130
|
+
) -> bool:
|
|
1131
|
+
match = _INLINE_FOLLOW_COMMAND_RE.match(str(text or "").strip())
|
|
1132
|
+
if not match:
|
|
1133
|
+
return False
|
|
1134
|
+
command = match.group(1).lower()
|
|
1135
|
+
args = (match.group(2) or "").strip()
|
|
1136
|
+
metadata = {"thread_id": thread_id} if thread_id else None
|
|
1137
|
+
if args:
|
|
1138
|
+
await self.send(chat_id, f"Usage: `/{command}`", reply_to=msg_id, metadata=metadata)
|
|
1139
|
+
return True
|
|
1140
|
+
|
|
1141
|
+
mode = "following" if command == "follow" else "unfollowed"
|
|
1142
|
+
target = {"userId": from_id} if chat_type == "dm" else {"chatId": chat_id}
|
|
1143
|
+
try:
|
|
1144
|
+
await self._sidecar_call("/follow-mode", {"target": target, "mode": mode})
|
|
1145
|
+
except Exception as exc:
|
|
1146
|
+
logger.warning("[inline] /%s failed for chat %s: %s", command, chat_id, exc)
|
|
1147
|
+
await self.send(
|
|
1148
|
+
chat_id,
|
|
1149
|
+
f"Could not update Inline follow mode. Try `/{command}` again.",
|
|
1150
|
+
reply_to=msg_id,
|
|
1151
|
+
metadata=metadata,
|
|
1152
|
+
)
|
|
1153
|
+
return True
|
|
1154
|
+
|
|
1155
|
+
self._chat_info_cache.pop(self._chat_key(chat_id), None)
|
|
1156
|
+
if command == "follow":
|
|
1157
|
+
body = (
|
|
1158
|
+
"Now explicitly following this Inline chat or thread. "
|
|
1159
|
+
"Eligible activity can wake Hermes without an @mention."
|
|
1160
|
+
)
|
|
1161
|
+
else:
|
|
1162
|
+
body = (
|
|
1163
|
+
"Explicitly unfollowed this Inline chat or thread. Automatic follow heuristics "
|
|
1164
|
+
"will not turn following back on; mentions and replies can still wake Hermes."
|
|
1165
|
+
)
|
|
1166
|
+
await self.send(chat_id, body, reply_to=msg_id, metadata=metadata)
|
|
1167
|
+
return True
|
|
1168
|
+
|
|
1084
1169
|
@property
|
|
1085
1170
|
def enforces_own_access_policy(self) -> bool:
|
|
1086
1171
|
"""Inline gates DM/group access at intake via dm_policy/group_policy."""
|
|
@@ -1481,6 +1566,16 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1481
1566
|
parent_chat_id=parent_chat_id,
|
|
1482
1567
|
):
|
|
1483
1568
|
return
|
|
1569
|
+
if await self._handle_follow_command(
|
|
1570
|
+
chat_id=chat_id,
|
|
1571
|
+
msg_id=msg_id,
|
|
1572
|
+
from_id=from_id,
|
|
1573
|
+
text=text,
|
|
1574
|
+
chat_type=chat_type,
|
|
1575
|
+
thread_id=thread_id,
|
|
1576
|
+
):
|
|
1577
|
+
return
|
|
1578
|
+
text = _normalize_inline_plugin_command_text(text)
|
|
1484
1579
|
reply_to_is_own = False
|
|
1485
1580
|
reply_to_text = None
|
|
1486
1581
|
reply_to_author = None
|
|
@@ -3667,6 +3762,290 @@ def _inline_threads_command_handler(raw_args: str) -> str:
|
|
|
3667
3762
|
)
|
|
3668
3763
|
|
|
3669
3764
|
|
|
3765
|
+
def _inline_follow_command_fallback(command: str, raw_args: str) -> str:
|
|
3766
|
+
if str(raw_args or "").strip():
|
|
3767
|
+
return f"Usage: `/{command}`"
|
|
3768
|
+
return (
|
|
3769
|
+
f"Inline follow mode is chat-scoped. Use `/{command}` inside the target "
|
|
3770
|
+
"Inline DM, group chat, or reply thread."
|
|
3771
|
+
)
|
|
3772
|
+
|
|
3773
|
+
|
|
3774
|
+
def _inline_follow_command_handler(raw_args: str = "") -> str:
|
|
3775
|
+
return _inline_follow_command_fallback("follow", raw_args)
|
|
3776
|
+
|
|
3777
|
+
|
|
3778
|
+
def _inline_unfollow_command_handler(raw_args: str = "") -> str:
|
|
3779
|
+
return _inline_follow_command_fallback("unfollow", raw_args)
|
|
3780
|
+
|
|
3781
|
+
|
|
3782
|
+
def _inline_update_environment() -> Dict[str, str]:
|
|
3783
|
+
try:
|
|
3784
|
+
from tools.environments.local import _sanitize_subprocess_env
|
|
3785
|
+
env = _sanitize_subprocess_env(os.environ.copy())
|
|
3786
|
+
except Exception:
|
|
3787
|
+
env = os.environ.copy()
|
|
3788
|
+
# Hermes cannot know the secret names introduced by every external plugin.
|
|
3789
|
+
sensitive_names = ("TOKEN", "SECRET", "PASSWORD", "API_KEY", "AUTHORIZATION")
|
|
3790
|
+
for key in list(env):
|
|
3791
|
+
if any(name in key.upper() for name in sensitive_names):
|
|
3792
|
+
env.pop(key, None)
|
|
3793
|
+
return env
|
|
3794
|
+
|
|
3795
|
+
|
|
3796
|
+
def _inline_update_log_text(raw: Any, hermes_home: Optional[Path] = None) -> str:
|
|
3797
|
+
text = str(raw or "")
|
|
3798
|
+
text = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", text)
|
|
3799
|
+
text = re.sub(r"(?i)\b(Bearer\s+)[^\s]+", r"\1[REDACTED]", text)
|
|
3800
|
+
text = re.sub(r"(?i)(https?://)[^/\s:@]+:[^@\s/]+@", r"\1[REDACTED]@", text)
|
|
3801
|
+
text = re.sub(r"([?&][^=\s&]+)=([^&\s]+)", r"\1=[REDACTED]", text)
|
|
3802
|
+
text = re.sub(
|
|
3803
|
+
r"(?i)\b([A-Za-z0-9_-]*(?:token|secret|password|api[_-]?key|authorization)[A-Za-z0-9_-]*)\s*([=:])\s*[^\s]+",
|
|
3804
|
+
r"\1\2[REDACTED]",
|
|
3805
|
+
text,
|
|
3806
|
+
)
|
|
3807
|
+
sensitive_names = ("TOKEN", "SECRET", "PASSWORD", "API_KEY", "AUTHORIZATION")
|
|
3808
|
+
for key, value in os.environ.items():
|
|
3809
|
+
if len(value) >= 8 and any(name in key.upper() for name in sensitive_names):
|
|
3810
|
+
text = text.replace(value, "[REDACTED]")
|
|
3811
|
+
private_paths = []
|
|
3812
|
+
if hermes_home:
|
|
3813
|
+
private_paths.append((str(hermes_home), "$HERMES_HOME"))
|
|
3814
|
+
private_paths.append((str(Path.home()), "~"))
|
|
3815
|
+
for path_text, replacement in private_paths:
|
|
3816
|
+
if len(path_text) > 1:
|
|
3817
|
+
text = text.replace(path_text, replacement)
|
|
3818
|
+
text = "".join(char if char in "\n\t" or ord(char) >= 32 else "?" for char in text)
|
|
3819
|
+
lines = [line.rstrip() for line in text.splitlines() if line.strip()]
|
|
3820
|
+
text = "\n".join(lines[-_INLINE_UPDATE_LOG_MAX_LINES:])
|
|
3821
|
+
if len(text) > _INLINE_UPDATE_LOG_MAX_CHARS:
|
|
3822
|
+
text = "[earlier output truncated]\n" + text[-_INLINE_UPDATE_LOG_MAX_CHARS:]
|
|
3823
|
+
return text or "(no subprocess output)"
|
|
3824
|
+
|
|
3825
|
+
|
|
3826
|
+
def _log_inline_update_failure(
|
|
3827
|
+
stage: str,
|
|
3828
|
+
detail: str,
|
|
3829
|
+
*,
|
|
3830
|
+
output: Any = None,
|
|
3831
|
+
hermes_home: Optional[Path] = None,
|
|
3832
|
+
) -> None:
|
|
3833
|
+
diagnostic = f"{detail}\n{output or ''}".strip()
|
|
3834
|
+
logger.error(
|
|
3835
|
+
"[inline-update] stage=%s failed\n%s",
|
|
3836
|
+
stage,
|
|
3837
|
+
_inline_update_log_text(diagnostic, hermes_home),
|
|
3838
|
+
)
|
|
3839
|
+
|
|
3840
|
+
|
|
3841
|
+
def _installed_inline_plugin_version(hermes_home: Path) -> Optional[str]:
|
|
3842
|
+
manifest = hermes_home / "plugins" / "inline" / "plugin.yaml"
|
|
3843
|
+
try:
|
|
3844
|
+
text = manifest.read_text(encoding="utf-8")
|
|
3845
|
+
except OSError:
|
|
3846
|
+
return None
|
|
3847
|
+
match = re.search(r"(?m)^version:\s*['\"]?([^\s'\"]+)", text)
|
|
3848
|
+
return match.group(1) if match else None
|
|
3849
|
+
|
|
3850
|
+
|
|
3851
|
+
def _inline_update_lane(version: Optional[str]) -> Optional[str]:
|
|
3852
|
+
if not version:
|
|
3853
|
+
return None
|
|
3854
|
+
match = re.fullmatch(r"\d+\.\d+\.\d+(?:-([A-Za-z][A-Za-z0-9-]*)(?:\.[0-9A-Za-z-]+)*)?", version)
|
|
3855
|
+
if not match:
|
|
3856
|
+
return None
|
|
3857
|
+
return match.group(1) or "latest"
|
|
3858
|
+
|
|
3859
|
+
|
|
3860
|
+
def _semver_core(version: Any) -> Optional[tuple[int, int, int]]:
|
|
3861
|
+
match = re.match(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$", str(version or "").strip())
|
|
3862
|
+
if not match:
|
|
3863
|
+
return None
|
|
3864
|
+
return tuple(int(part) for part in match.groups())
|
|
3865
|
+
|
|
3866
|
+
|
|
3867
|
+
def _run_inline_update() -> str:
|
|
3868
|
+
if not _INLINE_UPDATE_LOCK.acquire(blocking=False):
|
|
3869
|
+
return "An Inline plugin update is already running."
|
|
3870
|
+
try:
|
|
3871
|
+
return _run_inline_update_locked()
|
|
3872
|
+
finally:
|
|
3873
|
+
_INLINE_UPDATE_LOCK.release()
|
|
3874
|
+
|
|
3875
|
+
|
|
3876
|
+
def _run_inline_update_locked() -> str:
|
|
3877
|
+
hermes_home = Path(os.getenv("HERMES_HOME") or Path.home() / ".hermes").expanduser()
|
|
3878
|
+
npm_bin = shutil.which("npm")
|
|
3879
|
+
if not npm_bin:
|
|
3880
|
+
_log_inline_update_failure("precheck", "npm was not found on PATH", hermes_home=hermes_home)
|
|
3881
|
+
return "Inline plugin update is unavailable because npm was not found on PATH. Details were written to Hermes logs under `[inline-update]`."
|
|
3882
|
+
|
|
3883
|
+
target = hermes_home / "plugins" / "inline"
|
|
3884
|
+
if target.is_symlink():
|
|
3885
|
+
return (
|
|
3886
|
+
"Inline is installed as a development symlink, so automatic update was skipped. "
|
|
3887
|
+
"Update the linked source checkout and restart Hermes instead."
|
|
3888
|
+
)
|
|
3889
|
+
|
|
3890
|
+
installed_version = _installed_inline_plugin_version(hermes_home)
|
|
3891
|
+
lane = _inline_update_lane(installed_version)
|
|
3892
|
+
if not lane:
|
|
3893
|
+
return (
|
|
3894
|
+
"Inline plugin update could not determine the installed release channel. "
|
|
3895
|
+
"Update it manually with the package version or npm dist-tag you want to follow."
|
|
3896
|
+
)
|
|
3897
|
+
package_spec = f"{_INLINE_UPDATE_PACKAGE_NAME}@{lane}"
|
|
3898
|
+
precheck_command = [npm_bin, "view", package_spec, "--json"]
|
|
3899
|
+
try:
|
|
3900
|
+
precheck = subprocess.run(
|
|
3901
|
+
precheck_command,
|
|
3902
|
+
stdout=subprocess.PIPE,
|
|
3903
|
+
stderr=subprocess.PIPE,
|
|
3904
|
+
text=True,
|
|
3905
|
+
timeout=_INLINE_UPDATE_PRECHECK_TIMEOUT_SECONDS,
|
|
3906
|
+
check=False,
|
|
3907
|
+
env=_inline_update_environment(),
|
|
3908
|
+
)
|
|
3909
|
+
except subprocess.TimeoutExpired as exc:
|
|
3910
|
+
_log_inline_update_failure(
|
|
3911
|
+
"precheck",
|
|
3912
|
+
"npm view timed out",
|
|
3913
|
+
output=f"{exc.stdout or ''}\n{exc.stderr or ''}",
|
|
3914
|
+
hermes_home=hermes_home,
|
|
3915
|
+
)
|
|
3916
|
+
return "Inline plugin compatibility precheck timed out; no update was applied. Details were written to Hermes logs under `[inline-update]`."
|
|
3917
|
+
except OSError as exc:
|
|
3918
|
+
_log_inline_update_failure("precheck", f"npm view could not start: {exc}", hermes_home=hermes_home)
|
|
3919
|
+
return "Inline plugin compatibility precheck could not start; no update was applied. Details were written to Hermes logs under `[inline-update]`."
|
|
3920
|
+
if precheck.returncode != 0:
|
|
3921
|
+
_log_inline_update_failure(
|
|
3922
|
+
"precheck",
|
|
3923
|
+
f"npm view exited with code {precheck.returncode} for {package_spec}",
|
|
3924
|
+
output=f"{precheck.stdout or ''}\n{precheck.stderr or ''}",
|
|
3925
|
+
hermes_home=hermes_home,
|
|
3926
|
+
)
|
|
3927
|
+
return (
|
|
3928
|
+
f"Inline plugin compatibility precheck failed with exit code {precheck.returncode}; "
|
|
3929
|
+
"no update was applied. Details were written to Hermes logs under `[inline-update]`."
|
|
3930
|
+
)
|
|
3931
|
+
try:
|
|
3932
|
+
package_metadata = json.loads(precheck.stdout or "{}")
|
|
3933
|
+
except (TypeError, json.JSONDecodeError):
|
|
3934
|
+
package_metadata = None
|
|
3935
|
+
inline_metadata = package_metadata.get("inlineHermes") if isinstance(package_metadata, dict) else None
|
|
3936
|
+
candidate_version = package_metadata.get("version") if isinstance(package_metadata, dict) else None
|
|
3937
|
+
minimum_hermes = inline_metadata.get("minHermesVersion") if isinstance(inline_metadata, dict) else None
|
|
3938
|
+
try:
|
|
3939
|
+
from hermes_cli import __version__ as current_hermes
|
|
3940
|
+
except Exception:
|
|
3941
|
+
current_hermes = None
|
|
3942
|
+
current_core = _semver_core(current_hermes)
|
|
3943
|
+
minimum_core = _semver_core(minimum_hermes)
|
|
3944
|
+
if not candidate_version or not current_core or not minimum_core:
|
|
3945
|
+
_log_inline_update_failure(
|
|
3946
|
+
"precheck",
|
|
3947
|
+
f"invalid compatibility metadata for {package_spec}; current Hermes version={current_hermes!r}",
|
|
3948
|
+
output=precheck.stdout,
|
|
3949
|
+
hermes_home=hermes_home,
|
|
3950
|
+
)
|
|
3951
|
+
return (
|
|
3952
|
+
"Inline plugin compatibility metadata could not be verified; "
|
|
3953
|
+
"no update was applied. Details were written to Hermes logs under `[inline-update]`."
|
|
3954
|
+
)
|
|
3955
|
+
if current_core < minimum_core:
|
|
3956
|
+
return (
|
|
3957
|
+
f"Inline plugin `{candidate_version}` requires Hermes `{minimum_hermes}` or newer, "
|
|
3958
|
+
f"but this agent is running `{current_hermes}`. Update Hermes first; no plugin update was applied."
|
|
3959
|
+
)
|
|
3960
|
+
|
|
3961
|
+
command = [
|
|
3962
|
+
npm_bin,
|
|
3963
|
+
"exec",
|
|
3964
|
+
"--yes",
|
|
3965
|
+
f"--package={package_spec}",
|
|
3966
|
+
"--",
|
|
3967
|
+
"inline-hermes",
|
|
3968
|
+
"install",
|
|
3969
|
+
"--force",
|
|
3970
|
+
"--hermes-home",
|
|
3971
|
+
str(hermes_home),
|
|
3972
|
+
]
|
|
3973
|
+
try:
|
|
3974
|
+
result = subprocess.run(
|
|
3975
|
+
command,
|
|
3976
|
+
stdout=subprocess.PIPE,
|
|
3977
|
+
stderr=subprocess.STDOUT,
|
|
3978
|
+
text=True,
|
|
3979
|
+
timeout=_INLINE_UPDATE_TIMEOUT_SECONDS,
|
|
3980
|
+
check=False,
|
|
3981
|
+
env=_inline_update_environment(),
|
|
3982
|
+
)
|
|
3983
|
+
except subprocess.TimeoutExpired as exc:
|
|
3984
|
+
_log_inline_update_failure(
|
|
3985
|
+
"install",
|
|
3986
|
+
f"npm exec timed out for {package_spec}",
|
|
3987
|
+
output=exc.stdout,
|
|
3988
|
+
hermes_home=hermes_home,
|
|
3989
|
+
)
|
|
3990
|
+
return (
|
|
3991
|
+
"Inline plugin update timed out. Run "
|
|
3992
|
+
f"`npm exec --yes --package={package_spec} -- "
|
|
3993
|
+
"inline-hermes install --force` on the Hermes host. Details were written to Hermes logs under `[inline-update]`."
|
|
3994
|
+
)
|
|
3995
|
+
except OSError as exc:
|
|
3996
|
+
_log_inline_update_failure("install", f"npm exec could not start: {exc}", hermes_home=hermes_home)
|
|
3997
|
+
return "Inline plugin update could not start. Check that npm is installed and usable on the Hermes host. Details were written to Hermes logs under `[inline-update]`."
|
|
3998
|
+
|
|
3999
|
+
if result.returncode != 0:
|
|
4000
|
+
_log_inline_update_failure(
|
|
4001
|
+
"install",
|
|
4002
|
+
f"npm exec exited with code {result.returncode} for {package_spec}",
|
|
4003
|
+
output=result.stdout,
|
|
4004
|
+
hermes_home=hermes_home,
|
|
4005
|
+
)
|
|
4006
|
+
return (
|
|
4007
|
+
f"Inline plugin update failed with exit code {result.returncode}. Run "
|
|
4008
|
+
f"`npm exec --yes --package={package_spec} -- "
|
|
4009
|
+
"inline-hermes install --force` on the Hermes host. Details were written to Hermes logs under `[inline-update]`."
|
|
4010
|
+
)
|
|
4011
|
+
|
|
4012
|
+
version = _installed_inline_plugin_version(hermes_home)
|
|
4013
|
+
version_text = f" to `{version}`" if version else ""
|
|
4014
|
+
return f"Inline plugin updated{version_text} from the `{lane}` channel. Run `/restart` to load the new version."
|
|
4015
|
+
|
|
4016
|
+
|
|
4017
|
+
async def _inline_update_command_handler(raw_args: str = "") -> str:
|
|
4018
|
+
if str(raw_args or "").strip():
|
|
4019
|
+
return "Usage: `/inline_update`"
|
|
4020
|
+
return await asyncio.to_thread(_run_inline_update)
|
|
4021
|
+
|
|
4022
|
+
|
|
4023
|
+
def _inline_command_specs() -> tuple[_InlineCommandSpec, ...]:
|
|
4024
|
+
return (
|
|
4025
|
+
_InlineCommandSpec(
|
|
4026
|
+
name="threads",
|
|
4027
|
+
handler=_inline_threads_command_handler,
|
|
4028
|
+
description=_INLINE_THREADS_COMMAND_DESCRIPTION,
|
|
4029
|
+
args_hint=_INLINE_THREADS_COMMAND_ARGS,
|
|
4030
|
+
),
|
|
4031
|
+
_InlineCommandSpec(
|
|
4032
|
+
name="follow",
|
|
4033
|
+
handler=_inline_follow_command_handler,
|
|
4034
|
+
description=_INLINE_FOLLOW_COMMAND_DESCRIPTION,
|
|
4035
|
+
),
|
|
4036
|
+
_InlineCommandSpec(
|
|
4037
|
+
name="unfollow",
|
|
4038
|
+
handler=_inline_unfollow_command_handler,
|
|
4039
|
+
description=_INLINE_UNFOLLOW_COMMAND_DESCRIPTION,
|
|
4040
|
+
),
|
|
4041
|
+
_InlineCommandSpec(
|
|
4042
|
+
name="inline-update",
|
|
4043
|
+
handler=_inline_update_command_handler,
|
|
4044
|
+
description=_INLINE_UPDATE_COMMAND_DESCRIPTION,
|
|
4045
|
+
),
|
|
4046
|
+
)
|
|
4047
|
+
|
|
4048
|
+
|
|
3670
4049
|
def register(ctx) -> None:
|
|
3671
4050
|
from . import cli as _cli
|
|
3672
4051
|
from . import tools as _tools
|
|
@@ -3704,12 +4083,13 @@ def register(ctx) -> None:
|
|
|
3704
4083
|
)
|
|
3705
4084
|
register_command = getattr(ctx, "register_command", None)
|
|
3706
4085
|
if callable(register_command):
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
4086
|
+
for spec in _inline_command_specs():
|
|
4087
|
+
register_command(
|
|
4088
|
+
spec.name,
|
|
4089
|
+
handler=spec.handler,
|
|
4090
|
+
description=spec.description,
|
|
4091
|
+
args_hint=spec.args_hint,
|
|
4092
|
+
)
|
|
3713
4093
|
ctx.register_cli_command(
|
|
3714
4094
|
name="inline",
|
|
3715
4095
|
help="Set up and inspect the Inline integration",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: inline-platform
|
|
2
2
|
label: Inline
|
|
3
3
|
kind: platform
|
|
4
|
-
version: 0.0.5-alpha.
|
|
4
|
+
version: 0.0.5-alpha.5
|
|
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
|
|
@@ -24160,6 +24160,7 @@ function defaultErrorText(error) {
|
|
|
24160
24160
|
}
|
|
24161
24161
|
|
|
24162
24162
|
// src/sidecar/index.ts
|
|
24163
|
+
var DIALOG_FOLLOW_MODE_UNFOLLOWED = 2;
|
|
24163
24164
|
var token = process.env.INLINE_TOKEN || process.env.INLINE_BOT_TOKEN || "";
|
|
24164
24165
|
var baseUrl = process.env.INLINE_BASE_URL || "https://api.inline.chat";
|
|
24165
24166
|
var sidecarToken = process.env.INLINE_SIDECAR_TOKEN || "";
|
|
@@ -24307,6 +24308,9 @@ async function handleRequest(req, res) {
|
|
|
24307
24308
|
case "/chat":
|
|
24308
24309
|
await endpointChat(res, body);
|
|
24309
24310
|
return;
|
|
24311
|
+
case "/follow-mode":
|
|
24312
|
+
await endpointFollowMode(res, body);
|
|
24313
|
+
return;
|
|
24310
24314
|
case "/messages":
|
|
24311
24315
|
await endpointMessages(res, body);
|
|
24312
24316
|
return;
|
|
@@ -24523,6 +24527,23 @@ async function endpointChat(res, body) {
|
|
|
24523
24527
|
}
|
|
24524
24528
|
});
|
|
24525
24529
|
}
|
|
24530
|
+
async function endpointFollowMode(res, body) {
|
|
24531
|
+
const record = asRecord(body);
|
|
24532
|
+
const target = parseTarget(record);
|
|
24533
|
+
const mode = readRequiredString(record, "mode");
|
|
24534
|
+
const followMode = mode === "following" ? DialogFollowMode.FOLLOWING : mode === "unfollowed" ? DIALOG_FOLLOW_MODE_UNFOLLOWED : null;
|
|
24535
|
+
if (followMode == null) {
|
|
24536
|
+
throw new SidecarError("mode must be following or unfollowed", "bad_format");
|
|
24537
|
+
}
|
|
24538
|
+
await client.invokeUncheckedRaw(Method.UPDATE_DIALOG_FOLLOW_MODE, {
|
|
24539
|
+
oneofKind: "updateDialogFollowMode",
|
|
24540
|
+
updateDialogFollowMode: {
|
|
24541
|
+
peerId: inputPeerFromTarget(target),
|
|
24542
|
+
followMode
|
|
24543
|
+
}
|
|
24544
|
+
});
|
|
24545
|
+
writeJson(res, 200, { ok: true, result: { mode } });
|
|
24546
|
+
}
|
|
24526
24547
|
async function endpointMessages(res, body) {
|
|
24527
24548
|
const record = asRecord(body);
|
|
24528
24549
|
const target = parseTarget(record);
|