@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 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.14 inline-platform
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 `0.17.x`. This package was validated
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.14",
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",
@@ -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._sync_bot_commands()
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 _sync_bot_commands(self) -> None:
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) -> None:
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
 
@@ -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.14"
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 is configured!")
98
- hermes_setup.print_info("Restart the gateway when prompted, then message your bot in Inline.")
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
 
@@ -1,7 +1,7 @@
1
1
  name: inline-platform
2
2
  label: Inline
3
3
  kind: platform
4
- version: 0.0.14
4
+ version: 0.0.16
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