@inline-chat/hermes-agent-adapter 0.0.14 → 0.0.16
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 -4
- package/package.json +1 -1
- package/plugin/inline/adapter.py +155 -8
- package/plugin/inline/cli.py +5 -3
- package/plugin/inline/plugin.yaml +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Supported:
|
|
|
31
31
|
- Cached, privacy-safe sender names/usernames plus chat/thread IDs, selective reply/thread/observed context, and parent-thread context, with first-name/username Markdown mention guidance and current chat/thread 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`, `/follow`, `/unfollow`, `/inline_update`, and `/update`; typed slash commands continue to work even if menu sync is disabled or rejected.
|
|
34
|
+
- Native Inline `/` command-menu sync for Hermes slash commands, including `/threads`, `/follow`, `/unfollow`, `/inline_update`, `/inline_sync`, `/inline_version`, 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
|
- Agent-created `send_message`/`edit_message` button rows with opaque callback data. A callback is acknowledged immediately and becomes a normal Hermes turn naming the source message and exact action fields. The normal response edits that source message and clears omitted buttons; the agent can instead call `edit_message` with replacement buttons and finish with `NO_REPLY` so the explicit edit remains authoritative.
|
|
37
37
|
- Outbound local photo, video, voice, and document uploads with configurable size caps.
|
|
@@ -107,10 +107,20 @@ Check an installation:
|
|
|
107
107
|
|
|
108
108
|
```sh
|
|
109
109
|
inline-hermes doctor --json
|
|
110
|
-
hermes inline status
|
|
110
|
+
hermes inline status --json --probe
|
|
111
|
+
hermes gateway status
|
|
111
112
|
inline-hermes --version
|
|
112
113
|
```
|
|
113
114
|
|
|
115
|
+
Hermes publishes its installed skill catalog to Inline on gateway connect.
|
|
116
|
+
Running Hermes' `/reload-skills` now republishes both the native command menu
|
|
117
|
+
and skill catalog through the adapter's live refresh hook. From an authorized
|
|
118
|
+
Inline chat, `/inline_sync` is the direct recovery command for the same full
|
|
119
|
+
republish. After it succeeds, open or reopen Inline's Skilled Agent editor to
|
|
120
|
+
load it. `/inline_version` reports the loaded plugin and Hermes versions, the
|
|
121
|
+
installed/updated filesystem timestamp when available, and the last
|
|
122
|
+
in-process catalog-sync result without exposing paths, tokens, or config.
|
|
123
|
+
|
|
114
124
|
The Inline CLI can drive Hermes setup without putting the bot token in argv:
|
|
115
125
|
|
|
116
126
|
```sh
|
|
@@ -147,7 +157,7 @@ uv run ./hermes plugins list --plain --no-bundled
|
|
|
147
157
|
Expected local output includes:
|
|
148
158
|
|
|
149
159
|
```text
|
|
150
|
-
enabled user 0.0.
|
|
160
|
+
enabled user 0.0.16 inline-platform
|
|
151
161
|
```
|
|
152
162
|
|
|
153
163
|
## Update Or Reinstall
|
|
@@ -169,7 +179,7 @@ mismatch, rerun the same command after rebuilding or upgrading the package.
|
|
|
169
179
|
## Compatibility
|
|
170
180
|
|
|
171
181
|
- Hermes Agent: requires the external user plugin registry and native platform
|
|
172
|
-
plugin loader available in Hermes Agent
|
|
182
|
+
plugin loader available in Hermes Agent `>=0.17.0`. This package was validated
|
|
173
183
|
against Hermes Agent `0.21.0` from source commit `29112bef` (tag
|
|
174
184
|
`v2026.8.31`).
|
|
175
185
|
- Node.js: `>=20` is required for the bundled sidecar. Hermes-managed Node 22,
|
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.16",
|
|
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
|
@@ -107,6 +107,8 @@ _INLINE_THREADS_COMMAND_ARGS = "[status|on|off|auto|reset]"
|
|
|
107
107
|
_INLINE_FOLLOW_COMMAND_DESCRIPTION = "Explicitly follow this Inline chat or thread"
|
|
108
108
|
_INLINE_UNFOLLOW_COMMAND_DESCRIPTION = "Explicitly unfollow this Inline chat or thread"
|
|
109
109
|
_INLINE_UPDATE_COMMAND_DESCRIPTION = "Update the Inline Hermes plugin"
|
|
110
|
+
_INLINE_SYNC_COMMAND_DESCRIPTION = "Resync Inline commands and skills"
|
|
111
|
+
_INLINE_VERSION_COMMAND_DESCRIPTION = "Show Inline plugin and sync information"
|
|
110
112
|
_INLINE_UPDATE_PACKAGE_NAME = "@inline-chat/hermes-agent-adapter"
|
|
111
113
|
_INLINE_UPDATE_PRECHECK_TIMEOUT_SECONDS = 30
|
|
112
114
|
_INLINE_UPDATE_TIMEOUT_SECONDS = 5 * 60
|
|
@@ -117,6 +119,10 @@ _INLINE_THREADS_ACTION_PREFIX = "th:"
|
|
|
117
119
|
_INLINE_THREADS_ACTION_TTL_SECONDS = 15 * 60
|
|
118
120
|
_INLINE_THREAD_COMMAND_RE = re.compile(r"^/(?:thread|threads)(?:@[A-Za-z0-9_]+)?(?:\s+(.*))?$", re.IGNORECASE)
|
|
119
121
|
_INLINE_FOLLOW_COMMAND_RE = re.compile(r"^/(follow|unfollow)(?:@[A-Za-z0-9_]+)?(?:\s+(.*))?$", re.IGNORECASE)
|
|
122
|
+
_INLINE_MAINTENANCE_COMMAND_RE = re.compile(
|
|
123
|
+
r"^/(inline[_-](?:sync|version))(?:@[A-Za-z0-9_]+)?(?:\s+(.*))?$",
|
|
124
|
+
re.IGNORECASE,
|
|
125
|
+
)
|
|
120
126
|
_INLINE_REPLY_THREAD_NEGATION_RE = re.compile(
|
|
121
127
|
r"\b(?:do\s+not|don't|dont|please\s+don't|please\s+dont|no\s+need\s+to)\s+"
|
|
122
128
|
r"(?:create|start|open|make|use|move|take|reply|respond|answer|send|thread)\b[^.!?\n]*\bthread\b|"
|
|
@@ -970,6 +976,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
970
976
|
self._sidecar_proc: Optional[subprocess.Popen] = None
|
|
971
977
|
self._sidecar_supervisor_task: Optional[asyncio.Task] = None
|
|
972
978
|
self._command_sync_task: Optional[asyncio.Task] = None
|
|
979
|
+
self._catalog_sync_lock = asyncio.Lock()
|
|
980
|
+
self._last_catalog_sync: Optional[Dict[str, Any]] = None
|
|
973
981
|
self._inbound_task: Optional[asyncio.Task] = None
|
|
974
982
|
self._bot_settings_tasks: set[asyncio.Task] = set()
|
|
975
983
|
self._bot_settings_locks: Dict[str, asyncio.Lock] = {}
|
|
@@ -1842,6 +1850,44 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1842
1850
|
await self.send(chat_id, body, reply_to=msg_id, metadata=metadata)
|
|
1843
1851
|
return True
|
|
1844
1852
|
|
|
1853
|
+
async def _handle_inline_maintenance_command(
|
|
1854
|
+
self,
|
|
1855
|
+
*,
|
|
1856
|
+
chat_id: str,
|
|
1857
|
+
msg_id: str,
|
|
1858
|
+
text: str,
|
|
1859
|
+
thread_id: Optional[str],
|
|
1860
|
+
) -> bool:
|
|
1861
|
+
match = _INLINE_MAINTENANCE_COMMAND_RE.match(str(text or "").strip())
|
|
1862
|
+
if not match:
|
|
1863
|
+
return False
|
|
1864
|
+
command = match.group(1).lower().replace("-", "_")
|
|
1865
|
+
args = (match.group(2) or "").strip()
|
|
1866
|
+
metadata = {"thread_id": thread_id} if thread_id else None
|
|
1867
|
+
if args:
|
|
1868
|
+
await self.send(chat_id, f"Usage: `/{command}`", reply_to=msg_id, metadata=metadata)
|
|
1869
|
+
return True
|
|
1870
|
+
|
|
1871
|
+
if command == "inline_version":
|
|
1872
|
+
body = _inline_version_text(self._last_catalog_sync)
|
|
1873
|
+
else:
|
|
1874
|
+
status = await self._sync_inline_catalogs(reason="manual")
|
|
1875
|
+
failed = [name for name in ("commands", "skills") if status[name]["state"] == "failed"]
|
|
1876
|
+
if failed:
|
|
1877
|
+
body = (
|
|
1878
|
+
f"Inline catalog sync completed with failures in {', '.join(failed)}. "
|
|
1879
|
+
"Check Hermes logs for [inline] sync details."
|
|
1880
|
+
)
|
|
1881
|
+
else:
|
|
1882
|
+
body = (
|
|
1883
|
+
"Inline catalogs synced. "
|
|
1884
|
+
f"Commands: {_inline_catalog_part_summary(status['commands'])}. "
|
|
1885
|
+
f"Skills: {_inline_catalog_part_summary(status['skills'])}. "
|
|
1886
|
+
"Open or reopen the Skilled Agent editor in Inline to load the updated catalog."
|
|
1887
|
+
)
|
|
1888
|
+
await self.send(chat_id, body, reply_to=msg_id, metadata=metadata)
|
|
1889
|
+
return True
|
|
1890
|
+
|
|
1845
1891
|
@property
|
|
1846
1892
|
def enforces_own_access_policy(self) -> bool:
|
|
1847
1893
|
"""Inline gates DM/group access at intake via dm_policy/group_policy."""
|
|
@@ -2021,8 +2067,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2021
2067
|
|
|
2022
2068
|
async def _run_bot_command_sync(self) -> None:
|
|
2023
2069
|
try:
|
|
2024
|
-
await self.
|
|
2025
|
-
await self._sync_bot_skills()
|
|
2070
|
+
await self._sync_inline_catalogs(reason="gateway_start")
|
|
2026
2071
|
except asyncio.CancelledError:
|
|
2027
2072
|
raise
|
|
2028
2073
|
except Exception as exc:
|
|
@@ -2032,33 +2077,55 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2032
2077
|
if asyncio.current_task() is self._command_sync_task:
|
|
2033
2078
|
self._command_sync_task = None
|
|
2034
2079
|
|
|
2035
|
-
async def
|
|
2080
|
+
async def _sync_inline_catalogs(self, *, reason: str) -> Dict[str, Any]:
|
|
2081
|
+
async with self._catalog_sync_lock:
|
|
2082
|
+
commands = await self._sync_bot_commands()
|
|
2083
|
+
skills = await self._sync_bot_skills()
|
|
2084
|
+
status = {
|
|
2085
|
+
"reason": reason,
|
|
2086
|
+
"completed_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
2087
|
+
"commands": commands,
|
|
2088
|
+
"skills": skills,
|
|
2089
|
+
}
|
|
2090
|
+
self._last_catalog_sync = status
|
|
2091
|
+
return status
|
|
2092
|
+
|
|
2093
|
+
async def refresh_skill_group(self) -> tuple[int, int]:
|
|
2094
|
+
"""Republish Inline catalogs after Hermes `/reload-skills`."""
|
|
2095
|
+
status = await self._sync_inline_catalogs(reason="reload_skills")
|
|
2096
|
+
return (int(status["skills"].get("count") or 0), 0)
|
|
2097
|
+
|
|
2098
|
+
async def _sync_bot_commands(self) -> Dict[str, Any]:
|
|
2036
2099
|
if not self._sync_commands:
|
|
2037
|
-
return
|
|
2100
|
+
return {"state": "disabled", "count": 0}
|
|
2038
2101
|
if self._http_client is None:
|
|
2039
|
-
return
|
|
2102
|
+
return {"state": "failed", "count": 0}
|
|
2040
2103
|
try:
|
|
2041
2104
|
commands, hidden_count = _inline_menu_commands(max_commands=self._command_limit)
|
|
2042
2105
|
if not commands:
|
|
2043
2106
|
logger.warning("[inline] bot command sync skipped: no valid Hermes commands resolved")
|
|
2044
|
-
return
|
|
2107
|
+
return {"state": "failed", "count": 0}
|
|
2045
2108
|
synced = await self._set_bot_commands_with_retry(commands)
|
|
2046
2109
|
hidden_suffix = f", {hidden_count} hidden" if hidden_count else ""
|
|
2047
2110
|
logger.info("[inline] bot commands synced (%d command%s%s)", len(synced), "" if len(synced) == 1 else "s", hidden_suffix)
|
|
2111
|
+
return {"state": "synced", "count": len(synced), "hidden": hidden_count}
|
|
2048
2112
|
except Exception as exc:
|
|
2049
2113
|
self._report_error("commands.sync", exc)
|
|
2050
2114
|
logger.warning("[inline] bot command sync failed: %s", exc)
|
|
2115
|
+
return {"state": "failed", "count": 0}
|
|
2051
2116
|
|
|
2052
|
-
async def _sync_bot_skills(self) ->
|
|
2117
|
+
async def _sync_bot_skills(self) -> Dict[str, Any]:
|
|
2053
2118
|
if self._http_client is None:
|
|
2054
|
-
return
|
|
2119
|
+
return {"state": "failed", "count": 0}
|
|
2055
2120
|
try:
|
|
2056
2121
|
skills = _inline_skill_catalog()
|
|
2057
2122
|
await self._call_bot_api("setMySkills", {"skills": skills})
|
|
2058
2123
|
logger.info("[inline] bot skills synced (%d skill%s)", len(skills), "" if len(skills) == 1 else "s")
|
|
2124
|
+
return {"state": "synced", "count": len(skills)}
|
|
2059
2125
|
except Exception as exc:
|
|
2060
2126
|
self._report_error("skills.sync", exc)
|
|
2061
2127
|
logger.warning("[inline] bot skill sync failed: %s", exc)
|
|
2128
|
+
return {"state": "failed", "count": 0}
|
|
2062
2129
|
|
|
2063
2130
|
async def _set_bot_commands_with_retry(self, commands: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
2064
2131
|
try:
|
|
@@ -2360,6 +2427,13 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2360
2427
|
thread_id=thread_id,
|
|
2361
2428
|
):
|
|
2362
2429
|
return
|
|
2430
|
+
if not agent_action and await self._handle_inline_maintenance_command(
|
|
2431
|
+
chat_id=chat_id,
|
|
2432
|
+
msg_id=msg_id,
|
|
2433
|
+
text=text,
|
|
2434
|
+
thread_id=thread_id,
|
|
2435
|
+
):
|
|
2436
|
+
return
|
|
2363
2437
|
text = _normalize_inline_plugin_command_text(text)
|
|
2364
2438
|
reply_to_is_own = False
|
|
2365
2439
|
reply_to_text = None
|
|
@@ -5314,6 +5388,57 @@ def _installed_inline_plugin_version(hermes_home: Path) -> Optional[str]:
|
|
|
5314
5388
|
return match.group(1) if match else None
|
|
5315
5389
|
|
|
5316
5390
|
|
|
5391
|
+
def _inline_install_timestamp(hermes_home: Path) -> Optional[str]:
|
|
5392
|
+
target = hermes_home / "plugins" / "inline"
|
|
5393
|
+
try:
|
|
5394
|
+
timestamp = target.lstat().st_ctime
|
|
5395
|
+
except OSError:
|
|
5396
|
+
return None
|
|
5397
|
+
return datetime.fromtimestamp(timestamp, timezone.utc).isoformat().replace("+00:00", "Z")
|
|
5398
|
+
|
|
5399
|
+
|
|
5400
|
+
def _inline_catalog_part_summary(part: Dict[str, Any]) -> str:
|
|
5401
|
+
state = str(part.get("state") or "unknown")
|
|
5402
|
+
count = int(part.get("count") or 0)
|
|
5403
|
+
if state == "disabled":
|
|
5404
|
+
return "disabled"
|
|
5405
|
+
return f"{count} published" if state == "synced" else "failed"
|
|
5406
|
+
|
|
5407
|
+
|
|
5408
|
+
def _inline_version_text(last_sync: Optional[Dict[str, Any]] = None) -> str:
|
|
5409
|
+
hermes_home = Path(os.getenv("HERMES_HOME") or Path.home() / ".hermes").expanduser()
|
|
5410
|
+
version = _installed_inline_plugin_version(hermes_home)
|
|
5411
|
+
if not version:
|
|
5412
|
+
try:
|
|
5413
|
+
manifest = Path(__file__).with_name("plugin.yaml").read_text(encoding="utf-8")
|
|
5414
|
+
except OSError:
|
|
5415
|
+
manifest = ""
|
|
5416
|
+
match = re.search(r"(?m)^version:\s*['\"]?([^\s'\"]+)", manifest)
|
|
5417
|
+
version = match.group(1) if match else None
|
|
5418
|
+
try:
|
|
5419
|
+
from hermes_cli import __version__ as hermes_version
|
|
5420
|
+
except Exception:
|
|
5421
|
+
hermes_version = None
|
|
5422
|
+
|
|
5423
|
+
installed_at = _inline_install_timestamp(hermes_home)
|
|
5424
|
+
if last_sync:
|
|
5425
|
+
sync_text = (
|
|
5426
|
+
f"{last_sync.get('completed_at') or 'unknown'} "
|
|
5427
|
+
f"({last_sync.get('reason') or 'unknown'}; "
|
|
5428
|
+
f"commands {_inline_catalog_part_summary(last_sync.get('commands') or {})}; "
|
|
5429
|
+
f"skills {_inline_catalog_part_summary(last_sync.get('skills') or {})})"
|
|
5430
|
+
)
|
|
5431
|
+
else:
|
|
5432
|
+
sync_text = "not run in this process"
|
|
5433
|
+
return "\n".join([
|
|
5434
|
+
"Inline Hermes plugin",
|
|
5435
|
+
f"Plugin version: {version or 'unknown'}",
|
|
5436
|
+
f"Hermes version: {hermes_version or 'unknown'}",
|
|
5437
|
+
f"Installed or updated at: {installed_at or 'unavailable'} (filesystem metadata)",
|
|
5438
|
+
f"Last catalog sync: {sync_text}",
|
|
5439
|
+
])
|
|
5440
|
+
|
|
5441
|
+
|
|
5317
5442
|
def _inline_update_lane(version: Optional[str]) -> Optional[str]:
|
|
5318
5443
|
if not version:
|
|
5319
5444
|
return None
|
|
@@ -5486,6 +5611,18 @@ async def _inline_update_command_handler(raw_args: str = "") -> str:
|
|
|
5486
5611
|
return await asyncio.to_thread(_run_inline_update)
|
|
5487
5612
|
|
|
5488
5613
|
|
|
5614
|
+
async def _inline_sync_command_handler(raw_args: str = "") -> str:
|
|
5615
|
+
if str(raw_args or "").strip():
|
|
5616
|
+
return "Usage: `/inline_sync`"
|
|
5617
|
+
return "Run `/inline_sync` from a connected Inline chat to republish commands and skills."
|
|
5618
|
+
|
|
5619
|
+
|
|
5620
|
+
async def _inline_version_command_handler(raw_args: str = "") -> str:
|
|
5621
|
+
if str(raw_args or "").strip():
|
|
5622
|
+
return "Usage: `/inline_version`"
|
|
5623
|
+
return _inline_version_text()
|
|
5624
|
+
|
|
5625
|
+
|
|
5489
5626
|
def _inline_command_specs() -> tuple[_InlineCommandSpec, ...]:
|
|
5490
5627
|
return (
|
|
5491
5628
|
_InlineCommandSpec(
|
|
@@ -5509,6 +5646,16 @@ def _inline_command_specs() -> tuple[_InlineCommandSpec, ...]:
|
|
|
5509
5646
|
handler=_inline_update_command_handler,
|
|
5510
5647
|
description=_INLINE_UPDATE_COMMAND_DESCRIPTION,
|
|
5511
5648
|
),
|
|
5649
|
+
_InlineCommandSpec(
|
|
5650
|
+
name="inline-sync",
|
|
5651
|
+
handler=_inline_sync_command_handler,
|
|
5652
|
+
description=_INLINE_SYNC_COMMAND_DESCRIPTION,
|
|
5653
|
+
),
|
|
5654
|
+
_InlineCommandSpec(
|
|
5655
|
+
name="inline-version",
|
|
5656
|
+
handler=_inline_version_command_handler,
|
|
5657
|
+
description=_INLINE_VERSION_COMMAND_DESCRIPTION,
|
|
5658
|
+
),
|
|
5512
5659
|
)
|
|
5513
5660
|
|
|
5514
5661
|
|
package/plugin/inline/cli.py
CHANGED
|
@@ -27,7 +27,7 @@ _CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
|
|
|
27
27
|
_MAX_TOKEN_BYTES = 16 * 1024
|
|
28
28
|
_MAX_PROBE_RESPONSE_BYTES = 64 * 1024
|
|
29
29
|
_MACHINE_SETUP_PROTOCOL_VERSION = 1
|
|
30
|
-
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.
|
|
30
|
+
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.16"
|
|
31
31
|
_ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
|
|
32
32
|
|
|
33
33
|
|
|
@@ -94,8 +94,10 @@ def gateway_setup() -> None:
|
|
|
94
94
|
hermes_gateway.write_platform_config_field("inline", "enabled", True, raw=True)
|
|
95
95
|
|
|
96
96
|
print()
|
|
97
|
-
hermes_setup.print_success("💬 Inline
|
|
98
|
-
hermes_setup.print_info("Restart the gateway when prompted
|
|
97
|
+
hermes_setup.print_success("💬 Inline configuration saved.")
|
|
98
|
+
hermes_setup.print_info("Restart the gateway when prompted; setup is not ready until that restart succeeds.")
|
|
99
|
+
hermes_setup.print_info("Then run `hermes inline status --json --probe` to verify the bot credential.")
|
|
100
|
+
hermes_setup.print_info("After it reports ready, message your bot in Inline.")
|
|
99
101
|
hermes_setup.print_info("Send /sethome in that chat to use it for cron results and notifications.")
|
|
100
102
|
|
|
101
103
|
|