@inline-chat/hermes-agent-adapter 0.0.5-alpha.2 → 0.0.5-alpha.4
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 +10 -1
- package/package.json +1 -1
- package/plugin/inline/adapter.py +210 -0
- package/plugin/inline/plugin.yaml +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Supported:
|
|
|
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`, `/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.
|
|
@@ -325,6 +325,15 @@ rejects the full list with `BOT_COMMANDS_TOO_MUCH`, the adapter retries with a
|
|
|
325
325
|
smaller prefix. Menu sync failures are logged as warnings and do not prevent
|
|
326
326
|
message transport; `/commands` remains the full fallback list.
|
|
327
327
|
|
|
328
|
+
Run `/inline_update` to install the newest adapter from the plugin's current
|
|
329
|
+
npm release channel. Stable installs continue following `latest`, while
|
|
330
|
+
prerelease installs continue following their existing channel such as `alpha`
|
|
331
|
+
or `beta`. Before changing files, the command checks the selected release's
|
|
332
|
+
minimum Hermes version against the running agent and refuses incompatible or
|
|
333
|
+
unverifiable updates. The update runs in the background without exposing the
|
|
334
|
+
Inline token to npm, then asks you to run `/restart` so Hermes loads the
|
|
335
|
+
installed version. Development symlink installs are left untouched.
|
|
336
|
+
|
|
328
337
|
The plugin id is `inline`, which is intentionally the same id an eventual
|
|
329
338
|
bundled Hermes adapter should use.
|
|
330
339
|
|
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.4",
|
|
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,6 +20,7 @@ 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
|
|
@@ -81,10 +82,16 @@ _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_UPDATE_COMMAND_DESCRIPTION = "Update the Inline Hermes plugin"
|
|
86
|
+
_INLINE_UPDATE_PACKAGE_NAME = "@inline-chat/hermes-agent-adapter"
|
|
87
|
+
_INLINE_UPDATE_PRECHECK_TIMEOUT_SECONDS = 30
|
|
88
|
+
_INLINE_UPDATE_TIMEOUT_SECONDS = 5 * 60
|
|
89
|
+
_INLINE_UPDATE_LOCK = threading.Lock()
|
|
84
90
|
_INLINE_THREADS_ACTION_PREFIX = "th:"
|
|
85
91
|
_INLINE_THREADS_ACTION_TTL_SECONDS = 15 * 60
|
|
86
92
|
_INLINE_LOCAL_COMMANDS = (
|
|
87
93
|
("threads", _INLINE_THREADS_COMMAND_DESCRIPTION),
|
|
94
|
+
("inline_update", _INLINE_UPDATE_COMMAND_DESCRIPTION),
|
|
88
95
|
)
|
|
89
96
|
_INLINE_THREAD_COMMAND_RE = re.compile(r"^/(?:thread|threads)(?:@[A-Za-z0-9_]+)?(?:\s+(.*))?$", re.IGNORECASE)
|
|
90
97
|
_INLINE_REPLY_THREAD_NEGATION_RE = re.compile(
|
|
@@ -385,6 +392,22 @@ def _normalize_inline_command_name(raw: str) -> str:
|
|
|
385
392
|
return name.strip("_")
|
|
386
393
|
|
|
387
394
|
|
|
395
|
+
def _normalize_inline_plugin_command_text(text: str) -> str:
|
|
396
|
+
match = re.match(r"^/([a-z0-9_]+)(@[A-Za-z0-9_]+)?(\s.*)?$", str(text or ""), re.IGNORECASE | re.DOTALL)
|
|
397
|
+
if not match or "_" not in match.group(1):
|
|
398
|
+
return text
|
|
399
|
+
command = match.group(1).lower().replace("_", "-")
|
|
400
|
+
try:
|
|
401
|
+
from hermes_cli.plugins import get_plugin_commands
|
|
402
|
+
if command not in (get_plugin_commands() or {}):
|
|
403
|
+
return text
|
|
404
|
+
except Exception:
|
|
405
|
+
return text
|
|
406
|
+
# Inline menus require underscores, while Hermes registers plugin commands
|
|
407
|
+
# with hyphens. Normalize before gateway access checks as well as dispatch.
|
|
408
|
+
return f"/{command}{match.group(2) or ''}{match.group(3) or ''}"
|
|
409
|
+
|
|
410
|
+
|
|
388
411
|
def _normalize_inline_command_description(raw: str) -> str:
|
|
389
412
|
description = strip_markdown(str(raw or "")).strip()
|
|
390
413
|
description = re.sub(r"\s+", " ", description)
|
|
@@ -1390,6 +1413,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1390
1413
|
event = json.loads(line)
|
|
1391
1414
|
except json.JSONDecodeError:
|
|
1392
1415
|
return
|
|
1416
|
+
# Chat snapshots include mutable dialog and routing fields such as
|
|
1417
|
+
# followMode and lastMsgId. Invalidating here intentionally makes the
|
|
1418
|
+
# next group-message dispatch perform an extra GET_CHAT; correctness
|
|
1419
|
+
# across missed/reordered updates is worth that fetch until the sidecar
|
|
1420
|
+
# owns a coherent client-state cache.
|
|
1421
|
+
self._invalidate_chat_info(event.get("chatId"))
|
|
1393
1422
|
kind = event.get("kind")
|
|
1394
1423
|
if kind == "message.action.invoke":
|
|
1395
1424
|
if await self._handle_action(event):
|
|
@@ -1475,6 +1504,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1475
1504
|
parent_chat_id=parent_chat_id,
|
|
1476
1505
|
):
|
|
1477
1506
|
return
|
|
1507
|
+
text = _normalize_inline_plugin_command_text(text)
|
|
1478
1508
|
reply_to_is_own = False
|
|
1479
1509
|
reply_to_text = None
|
|
1480
1510
|
reply_to_author = None
|
|
@@ -2438,6 +2468,11 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2438
2468
|
if len(self._visible_reply_thread_targets) > _CHAT_INFO_CACHE_MAX_SIZE:
|
|
2439
2469
|
self._visible_reply_thread_targets.popitem(last=False)
|
|
2440
2470
|
|
|
2471
|
+
def _invalidate_chat_info(self, chat_id: Any) -> None:
|
|
2472
|
+
key = self._chat_key(chat_id)
|
|
2473
|
+
if key:
|
|
2474
|
+
self._chat_info_cache.pop(key, None)
|
|
2475
|
+
|
|
2441
2476
|
def _reply_to_for_target(self, reply_to: Optional[str], target: Dict[str, str]) -> Optional[str]:
|
|
2442
2477
|
if not reply_to:
|
|
2443
2478
|
return None
|
|
@@ -3511,6 +3546,9 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3511
3546
|
try:
|
|
3512
3547
|
data = await self._sidecar_call(path, body)
|
|
3513
3548
|
result = data.get("result") or {}
|
|
3549
|
+
target = body.get("target")
|
|
3550
|
+
if isinstance(target, dict):
|
|
3551
|
+
self._invalidate_chat_info(target.get("chatId"))
|
|
3514
3552
|
return _send_result(
|
|
3515
3553
|
success=True,
|
|
3516
3554
|
message_id=str(result.get("messageId") or "") or None,
|
|
@@ -3653,6 +3691,173 @@ def _inline_threads_command_handler(raw_args: str) -> str:
|
|
|
3653
3691
|
)
|
|
3654
3692
|
|
|
3655
3693
|
|
|
3694
|
+
def _inline_update_environment() -> Dict[str, str]:
|
|
3695
|
+
try:
|
|
3696
|
+
from tools.environments.local import _sanitize_subprocess_env
|
|
3697
|
+
env = _sanitize_subprocess_env(os.environ.copy())
|
|
3698
|
+
except Exception:
|
|
3699
|
+
env = os.environ.copy()
|
|
3700
|
+
# Hermes cannot know the secret names introduced by every external plugin.
|
|
3701
|
+
sensitive_names = ("TOKEN", "SECRET", "PASSWORD", "API_KEY", "AUTHORIZATION")
|
|
3702
|
+
for key in list(env):
|
|
3703
|
+
if any(name in key.upper() for name in sensitive_names):
|
|
3704
|
+
env.pop(key, None)
|
|
3705
|
+
return env
|
|
3706
|
+
|
|
3707
|
+
|
|
3708
|
+
def _installed_inline_plugin_version(hermes_home: Path) -> Optional[str]:
|
|
3709
|
+
manifest = hermes_home / "plugins" / "inline" / "plugin.yaml"
|
|
3710
|
+
try:
|
|
3711
|
+
text = manifest.read_text(encoding="utf-8")
|
|
3712
|
+
except OSError:
|
|
3713
|
+
return None
|
|
3714
|
+
match = re.search(r"(?m)^version:\s*['\"]?([^\s'\"]+)", text)
|
|
3715
|
+
return match.group(1) if match else None
|
|
3716
|
+
|
|
3717
|
+
|
|
3718
|
+
def _inline_update_lane(version: Optional[str]) -> Optional[str]:
|
|
3719
|
+
if not version:
|
|
3720
|
+
return None
|
|
3721
|
+
match = re.fullmatch(r"\d+\.\d+\.\d+(?:-([A-Za-z][A-Za-z0-9-]*)(?:\.[0-9A-Za-z-]+)*)?", version)
|
|
3722
|
+
if not match:
|
|
3723
|
+
return None
|
|
3724
|
+
return match.group(1) or "latest"
|
|
3725
|
+
|
|
3726
|
+
|
|
3727
|
+
def _semver_core(version: Any) -> Optional[tuple[int, int, int]]:
|
|
3728
|
+
match = re.match(r"^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$", str(version or "").strip())
|
|
3729
|
+
if not match:
|
|
3730
|
+
return None
|
|
3731
|
+
return tuple(int(part) for part in match.groups())
|
|
3732
|
+
|
|
3733
|
+
|
|
3734
|
+
def _run_inline_update() -> str:
|
|
3735
|
+
if not _INLINE_UPDATE_LOCK.acquire(blocking=False):
|
|
3736
|
+
return "An Inline plugin update is already running."
|
|
3737
|
+
try:
|
|
3738
|
+
return _run_inline_update_locked()
|
|
3739
|
+
finally:
|
|
3740
|
+
_INLINE_UPDATE_LOCK.release()
|
|
3741
|
+
|
|
3742
|
+
|
|
3743
|
+
def _run_inline_update_locked() -> str:
|
|
3744
|
+
npm_bin = shutil.which("npm")
|
|
3745
|
+
if not npm_bin:
|
|
3746
|
+
return "Inline plugin update is unavailable because npm was not found on PATH."
|
|
3747
|
+
|
|
3748
|
+
hermes_home = Path(os.getenv("HERMES_HOME") or Path.home() / ".hermes").expanduser()
|
|
3749
|
+
target = hermes_home / "plugins" / "inline"
|
|
3750
|
+
if target.is_symlink():
|
|
3751
|
+
return (
|
|
3752
|
+
"Inline is installed as a development symlink, so automatic update was skipped. "
|
|
3753
|
+
"Update the linked source checkout and restart Hermes instead."
|
|
3754
|
+
)
|
|
3755
|
+
|
|
3756
|
+
installed_version = _installed_inline_plugin_version(hermes_home)
|
|
3757
|
+
lane = _inline_update_lane(installed_version)
|
|
3758
|
+
if not lane:
|
|
3759
|
+
return (
|
|
3760
|
+
"Inline plugin update could not determine the installed release channel. "
|
|
3761
|
+
"Update it manually with the package version or npm dist-tag you want to follow."
|
|
3762
|
+
)
|
|
3763
|
+
package_spec = f"{_INLINE_UPDATE_PACKAGE_NAME}@{lane}"
|
|
3764
|
+
precheck_command = [npm_bin, "view", package_spec, "--json"]
|
|
3765
|
+
try:
|
|
3766
|
+
precheck = subprocess.run(
|
|
3767
|
+
precheck_command,
|
|
3768
|
+
stdout=subprocess.PIPE,
|
|
3769
|
+
stderr=subprocess.PIPE,
|
|
3770
|
+
text=True,
|
|
3771
|
+
timeout=_INLINE_UPDATE_PRECHECK_TIMEOUT_SECONDS,
|
|
3772
|
+
check=False,
|
|
3773
|
+
env=_inline_update_environment(),
|
|
3774
|
+
)
|
|
3775
|
+
except subprocess.TimeoutExpired:
|
|
3776
|
+
return "Inline plugin compatibility precheck timed out; no update was applied."
|
|
3777
|
+
except OSError as exc:
|
|
3778
|
+
logger.warning("[inline] plugin compatibility precheck failed to start: %s", exc)
|
|
3779
|
+
return "Inline plugin compatibility precheck could not start; no update was applied."
|
|
3780
|
+
if precheck.returncode != 0:
|
|
3781
|
+
logger.warning("[inline] plugin compatibility precheck exited with code %s", precheck.returncode)
|
|
3782
|
+
return (
|
|
3783
|
+
f"Inline plugin compatibility precheck failed with exit code {precheck.returncode}; "
|
|
3784
|
+
"no update was applied."
|
|
3785
|
+
)
|
|
3786
|
+
try:
|
|
3787
|
+
package_metadata = json.loads(precheck.stdout or "{}")
|
|
3788
|
+
except (TypeError, json.JSONDecodeError):
|
|
3789
|
+
package_metadata = None
|
|
3790
|
+
inline_metadata = package_metadata.get("inlineHermes") if isinstance(package_metadata, dict) else None
|
|
3791
|
+
candidate_version = package_metadata.get("version") if isinstance(package_metadata, dict) else None
|
|
3792
|
+
minimum_hermes = inline_metadata.get("minHermesVersion") if isinstance(inline_metadata, dict) else None
|
|
3793
|
+
try:
|
|
3794
|
+
from hermes_cli import __version__ as current_hermes
|
|
3795
|
+
except Exception:
|
|
3796
|
+
current_hermes = None
|
|
3797
|
+
current_core = _semver_core(current_hermes)
|
|
3798
|
+
minimum_core = _semver_core(minimum_hermes)
|
|
3799
|
+
if not candidate_version or not current_core or not minimum_core:
|
|
3800
|
+
return (
|
|
3801
|
+
"Inline plugin compatibility metadata could not be verified; "
|
|
3802
|
+
"no update was applied."
|
|
3803
|
+
)
|
|
3804
|
+
if current_core < minimum_core:
|
|
3805
|
+
return (
|
|
3806
|
+
f"Inline plugin `{candidate_version}` requires Hermes `{minimum_hermes}` or newer, "
|
|
3807
|
+
f"but this agent is running `{current_hermes}`. Update Hermes first; no plugin update was applied."
|
|
3808
|
+
)
|
|
3809
|
+
|
|
3810
|
+
command = [
|
|
3811
|
+
npm_bin,
|
|
3812
|
+
"exec",
|
|
3813
|
+
"--yes",
|
|
3814
|
+
f"--package={package_spec}",
|
|
3815
|
+
"--",
|
|
3816
|
+
"inline-hermes",
|
|
3817
|
+
"install",
|
|
3818
|
+
"--force",
|
|
3819
|
+
"--hermes-home",
|
|
3820
|
+
str(hermes_home),
|
|
3821
|
+
]
|
|
3822
|
+
try:
|
|
3823
|
+
result = subprocess.run(
|
|
3824
|
+
command,
|
|
3825
|
+
stdout=subprocess.PIPE,
|
|
3826
|
+
stderr=subprocess.STDOUT,
|
|
3827
|
+
text=True,
|
|
3828
|
+
timeout=_INLINE_UPDATE_TIMEOUT_SECONDS,
|
|
3829
|
+
check=False,
|
|
3830
|
+
env=_inline_update_environment(),
|
|
3831
|
+
)
|
|
3832
|
+
except subprocess.TimeoutExpired:
|
|
3833
|
+
return (
|
|
3834
|
+
"Inline plugin update timed out. Run "
|
|
3835
|
+
f"`npm exec --yes --package={package_spec} -- "
|
|
3836
|
+
"inline-hermes install --force` on the Hermes host."
|
|
3837
|
+
)
|
|
3838
|
+
except OSError as exc:
|
|
3839
|
+
logger.warning("[inline] plugin update failed to start: %s", exc)
|
|
3840
|
+
return "Inline plugin update could not start. Check that npm is installed and usable on the Hermes host."
|
|
3841
|
+
|
|
3842
|
+
if result.returncode != 0:
|
|
3843
|
+
logger.warning("[inline] plugin update exited with code %s", result.returncode)
|
|
3844
|
+
return (
|
|
3845
|
+
f"Inline plugin update failed with exit code {result.returncode}. Run "
|
|
3846
|
+
f"`npm exec --yes --package={package_spec} -- "
|
|
3847
|
+
"inline-hermes install --force` on the Hermes host."
|
|
3848
|
+
)
|
|
3849
|
+
|
|
3850
|
+
version = _installed_inline_plugin_version(hermes_home)
|
|
3851
|
+
version_text = f" to `{version}`" if version else ""
|
|
3852
|
+
return f"Inline plugin updated{version_text} from the `{lane}` channel. Run `/restart` to load the new version."
|
|
3853
|
+
|
|
3854
|
+
|
|
3855
|
+
async def _inline_update_command_handler(raw_args: str = "") -> str:
|
|
3856
|
+
if str(raw_args or "").strip():
|
|
3857
|
+
return "Usage: `/inline_update`"
|
|
3858
|
+
return await asyncio.to_thread(_run_inline_update)
|
|
3859
|
+
|
|
3860
|
+
|
|
3656
3861
|
def register(ctx) -> None:
|
|
3657
3862
|
from . import cli as _cli
|
|
3658
3863
|
from . import tools as _tools
|
|
@@ -3696,6 +3901,11 @@ def register(ctx) -> None:
|
|
|
3696
3901
|
description=_INLINE_THREADS_COMMAND_DESCRIPTION,
|
|
3697
3902
|
args_hint=_INLINE_THREADS_COMMAND_ARGS,
|
|
3698
3903
|
)
|
|
3904
|
+
register_command(
|
|
3905
|
+
"inline-update",
|
|
3906
|
+
handler=_inline_update_command_handler,
|
|
3907
|
+
description=_INLINE_UPDATE_COMMAND_DESCRIPTION,
|
|
3908
|
+
)
|
|
3699
3909
|
ctx.register_cli_command(
|
|
3700
3910
|
name="inline",
|
|
3701
3911
|
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.4
|
|
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
|