@inline-chat/hermes-agent-adapter 0.0.18-alpha.0 → 0.0.18

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
@@ -157,7 +157,7 @@ uv run ./hermes plugins list --plain --no-bundled
157
157
  Expected local output includes:
158
158
 
159
159
  ```text
160
- enabled user 0.0.18-alpha.0 inline-platform
160
+ enabled user 0.0.18 inline-platform
161
161
  ```
162
162
 
163
163
  ## Update Or Reinstall
@@ -179,13 +179,17 @@ mismatch, rerun the same command after rebuilding or upgrading the package.
179
179
  ## Compatibility
180
180
 
181
181
  - Hermes Agent: requires the external user plugin registry and native platform
182
- plugin loader available in Hermes Agent `>=0.17.0`. This package was validated
183
- against Hermes Agent `0.21.0` from source commit `29112bef` (tag
184
- `v2026.8.31`).
182
+ plugin loader available in Hermes Agent `>=0.21.4`. This package was validated
183
+ against Hermes Agent `0.21.5` from source commit `f97608f178d1ffeca59860195ab7da295f7c8e5f` (tag
184
+ `v2026.9.24`).
185
+ - CI checks the minimum supported Hermes release, newest stable source, and upstream
186
+ `main`. A six-hour scheduled check also tests the published npm `latest` adapter.
187
+ `inline-hermes doctor` requires successful host loading and compatibility validation;
188
+ unavailable Hermes diagnostics no longer count as healthy.
185
189
  - Node.js: `>=20` is required for the bundled sidecar. Hermes-managed Node 22,
186
190
  system Node, or an explicit `INLINE_NODE_BIN` path all work.
187
191
  - Inbound recovery retries without waiting for another message or reconnect. Independent chats are consumed concurrently; same-chat order and delivery acknowledgements are preserved. Sender provenance lookups start with a short timeout and expand up to the existing SDK ceiling after timeouts; deferred inputs stay recoverable. Stream replacement wakes pending backpressure writes.
188
- - Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.19-alpha.0` and is
192
+ - Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.18` and is
189
193
  bundled into the npm package, so Hermes startup does not run `npm install`.
190
194
  - Live sends require a valid Inline user or bot token in `INLINE_TOKEN`,
191
195
  `INLINE_BOT_TOKEN`, `platforms.inline.token`, or `inline.token`.
package/dist/install.js CHANGED
@@ -7174,6 +7174,12 @@ async function inspectInstall(params) {
7174
7174
  if (!node.ok) {
7175
7175
  issues.push(node.error || `${node.source} is not usable: ${node.path}`);
7176
7176
  }
7177
+ if (params.opts.command === "doctor" && !activation.hermesCompatibilityVerified) {
7178
+ issues.push("Hermes could not validate the installed plugin compatibility; check `hermes inline status --json --check-compatibility` and update or repair the plugin");
7179
+ }
7180
+ if (params.opts.command === "doctor" && !activation.hermesCredentialStoreChecked) {
7181
+ issues.push("Hermes credential verification is unavailable; doctor cannot confirm runtime readiness");
7182
+ }
7177
7183
  if (params.opts.command === "doctor" && !activation.pluginEnabled) {
7178
7184
  issues.push(`Hermes plugin '${pluginEnableKey}' is not enabled. Run: hermes plugins enable ${pluginEnableKey}`);
7179
7185
  }
@@ -7579,7 +7585,7 @@ async function inspectActivation(hermesHome, target, probeCredential) {
7579
7585
  }
7580
7586
  const tokenPresent = Boolean(process.env.INLINE_TOKEN || process.env.INLINE_BOT_TOKEN);
7581
7587
  const configTokenConfigured = configTokenIsConfigured(config, ["platforms", defaultPluginId, "token"]) || configTokenIsConfigured(config, [defaultPluginId, "token"]);
7582
- const machineCredential = inspectHermesCredential(hermesHome, probeCredential);
7588
+ const machineCredential = inspectHermesCredential(hermesHome, probeCredential, target);
7583
7589
  const fallbackConfigured = tokenPresent || configTokenConfigured;
7584
7590
  const credentialState = (machineCredential.state === "unknown" || machineCredential.state === "not_configured") && fallbackConfigured ? "configured_unverified" : machineCredential.state;
7585
7591
  const installOwnershipAligned = await inspectInstallOwnership(hermesHome, target);
@@ -7589,6 +7595,7 @@ async function inspectActivation(hermesHome, target, probeCredential) {
7589
7595
  pluginEnabled: readConfigList(config, ["plugins", "enabled"]).includes(pluginEnableKey),
7590
7596
  platformConfigured: readConfigBoolean(config, ["platforms", defaultPluginId, "enabled"]) || readConfigBoolean(config, [defaultPluginId, "enabled"]),
7591
7597
  configTokenConfigured,
7598
+ hermesCompatibilityVerified: machineCredential.compatible === true,
7592
7599
  hermesCredentialStoreChecked: machineCredential.checked,
7593
7600
  hermesCredentialStoreTokenConfigured: machineCredential.configured === true,
7594
7601
  credentialState,
@@ -7649,9 +7656,9 @@ async function chownTree(target, uid, gid) {
7649
7656
  }
7650
7657
  await chown(target, uid, gid);
7651
7658
  }
7652
- function inspectHermesCredential(hermesHome, probe) {
7659
+ function inspectHermesCredential(hermesHome, probe, target) {
7653
7660
  const executable = normalizeToken(process.env.INLINE_HERMES_BIN) || "hermes";
7654
- const args = ["inline", "status", "--json", ...probe ? ["--probe"] : []];
7661
+ const args = ["inline", "status", "--json", ...probe ? ["--probe", "--check-compatibility"] : []];
7655
7662
  const result = spawnSync(executable, args, {
7656
7663
  encoding: "utf8",
7657
7664
  timeout: hermesMachineTimeoutMs,
@@ -7675,11 +7682,13 @@ function inspectHermesCredential(hermesHome, probe) {
7675
7682
  if (record.action !== "inline.status" || typeof record.setupProtocolVersion !== "number" || record.setupProtocolVersion < 1 || typeof record.configured !== "boolean") {
7676
7683
  return { checked: false, configured: null, state: "unknown" };
7677
7684
  }
7685
+ const compatibility = record.compatibility;
7686
+ const compatible = result.status === 0 && compatibility?.ok === true && compatibility.pluginPath === safeRealpathSync(target);
7678
7687
  if (!record.configured) {
7679
- return { checked: true, configured: false, state: "not_configured" };
7688
+ return { compatible, checked: true, configured: false, state: "not_configured" };
7680
7689
  }
7681
7690
  if (!probe) {
7682
- return { checked: true, configured: true, state: "configured_unverified" };
7691
+ return { compatible, checked: true, configured: true, state: "configured_unverified" };
7683
7692
  }
7684
7693
  const probeResult = record.probe && typeof record.probe === "object" && !Array.isArray(record.probe) ? record.probe : null;
7685
7694
  if (probeResult?.ok === true) {
@@ -7688,6 +7697,7 @@ function inspectHermesCredential(hermesHome, probe) {
7688
7697
  return { checked: false, configured: true, state: "unknown" };
7689
7698
  const botUsername = typeof probeResult.botUsername === "string" && probeResult.botUsername.trim() ? probeResult.botUsername.trim() : undefined;
7690
7699
  return {
7700
+ compatible,
7691
7701
  checked: true,
7692
7702
  configured: true,
7693
7703
  state: "verified",
@@ -7696,9 +7706,16 @@ function inspectHermesCredential(hermesHome, probe) {
7696
7706
  };
7697
7707
  }
7698
7708
  if (probeResult?.errorKind === "invalid_credential") {
7699
- return { checked: true, configured: true, state: "invalid" };
7709
+ return { compatible, checked: true, configured: true, state: "invalid" };
7710
+ }
7711
+ return { compatible, checked: true, configured: true, state: "unknown" };
7712
+ }
7713
+ function safeRealpathSync(target) {
7714
+ try {
7715
+ return realpathSync(target);
7716
+ } catch {
7717
+ return null;
7700
7718
  }
7701
- return { checked: true, configured: true, state: "unknown" };
7702
7719
  }
7703
7720
  function readConfigList(config, pathParts) {
7704
7721
  const value = readConfigValue(config, pathParts);
@@ -8118,6 +8135,7 @@ Options:
8118
8135
  console.log(`Usage: inline-hermes ${topic} [options]
8119
8136
 
8120
8137
  ${topic === "doctor" ? "Diagnose" : "Inspect"} the installed Inline plugin, sidecar integrity, and Node runtime.
8138
+ ${topic === "doctor" ? "Doctor also requires a successful Hermes loader compatibility check and credential probe." : ""}
8121
8139
 
8122
8140
  Options:
8123
8141
  --hermes-home <path> Hermes home directory. Defaults to HERMES_HOME or ~/.hermes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inline-chat/hermes-agent-adapter",
3
- "version": "0.0.18-alpha.0",
3
+ "version": "0.0.18",
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",
@@ -62,7 +62,7 @@
62
62
  "check": "bun run typecheck && bun run lint && bun run test"
63
63
  },
64
64
  "dependencies": {
65
- "@inline-chat/realtime-sdk": "0.0.19-alpha.0",
65
+ "@inline-chat/realtime-sdk": "0.0.18",
66
66
  "yaml": "2.9.0"
67
67
  },
68
68
  "devDependencies": {
@@ -81,8 +81,8 @@
81
81
  "npmSpec": "@inline-chat/hermes-agent-adapter"
82
82
  },
83
83
  "machineSetupProtocol": 1,
84
- "minHermesVersion": "0.17.0",
85
- "testedHermesVersion": "0.21.0",
86
- "testedHermesCommit": "29112bef"
84
+ "minHermesVersion": "0.21.4",
85
+ "testedHermesVersion": "0.21.5",
86
+ "testedHermesCommit": "f97608f178d1ffeca59860195ab7da295f7c8e5f"
87
87
  }
88
88
  }
@@ -45,6 +45,7 @@ from gateway.platforms.base import (
45
45
  SendResult,
46
46
  cache_audio_from_url,
47
47
  cache_image_from_url,
48
+ safe_url_for_log,
48
49
  )
49
50
  from gateway.platforms.helpers import strip_markdown
50
51
 
@@ -575,7 +576,7 @@ def _normalize_inline_command_description(raw: str) -> str:
575
576
 
576
577
 
577
578
  def _inline_menu_commands(max_commands: int = _INLINE_COMMAND_LIMIT) -> tuple[List[Dict[str, Any]], int]:
578
- from hermes_cli.commands import telegram_menu_commands
579
+ from hermes_cli.commands_platforms import telegram_menu_commands
579
580
 
580
581
  command_specs = _inline_command_specs()
581
582
  local_commands = [
@@ -1209,7 +1210,7 @@ class InlineAdapter(BasePlatformAdapter):
1209
1210
 
1210
1211
  try:
1211
1212
  from gateway.run import _load_gateway_config
1212
- from hermes_cli.model_switch import list_picker_providers
1213
+ from hermes_cli.model_switch_providers import list_picker_providers
1213
1214
 
1214
1215
  cfg = _load_gateway_config() or {}
1215
1216
  model_cfg = cfg.get("model") if isinstance(cfg, dict) else {}
@@ -3001,16 +3002,15 @@ class InlineAdapter(BasePlatformAdapter):
3001
3002
  return await cache_audio_from_url(url, ext=_extension_for_media(mime, file_name, ".ogg"))
3002
3003
  return await self._download_inline_media_url(url, mime=mime, file_name=file_name)
3003
3004
  except Exception as exc:
3004
- logger.warning("[inline] failed to cache %s attachment: %s", kind, exc)
3005
- return url
3005
+ logger.warning("[inline] failed to cache %s attachment (%s)", kind, type(exc).__name__)
3006
+ # Never pass an unvalidated URL on to Hermes for another fetch.
3007
+ return None
3006
3008
 
3007
3009
  async def _download_inline_media_url(self, url: str, *, mime: str, file_name: Optional[str]) -> str:
3008
- try:
3009
- from tools.url_safety import is_safe_url, safe_url_for_log
3010
- if not is_safe_url(url):
3011
- raise ValueError(f"blocked unsafe media URL: {safe_url_for_log(url)}")
3012
- except ImportError:
3013
- pass
3010
+ # A missing host safety helper must stop the download, never bypass it.
3011
+ from tools.url_safety import is_safe_url
3012
+ if not is_safe_url(url):
3013
+ raise ValueError(f"blocked unsafe media URL: {safe_url_for_log(url)}")
3014
3014
  _MEDIA_CACHE_DIR.mkdir(parents=True, exist_ok=True)
3015
3015
  name = _safe_media_file_name(url=url, mime=mime, file_name=file_name)
3016
3016
  path = _MEDIA_CACHE_DIR / name
@@ -4230,7 +4230,7 @@ class InlineAdapter(BasePlatformAdapter):
4230
4230
  label = group_id
4231
4231
  member_slugs: list[str] = []
4232
4232
  try:
4233
- from hermes_cli.models import PROVIDER_GROUPS
4233
+ from hermes_cli.models_catalog_static import PROVIDER_GROUPS
4234
4234
  label, _desc, members = PROVIDER_GROUPS.get(group_id, (group_id, "", []))
4235
4235
  member_slugs = [str(member) for member in members]
4236
4236
  except Exception:
@@ -4686,7 +4686,6 @@ class InlineAdapter(BasePlatformAdapter):
4686
4686
 
4687
4687
  async def send_image(self, chat_id: str, image_url: str, caption: Optional[str] = None, reply_to: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> SendResult:
4688
4688
  try:
4689
- from gateway.platforms.base import cache_image_from_url
4690
4689
  local_path = await cache_image_from_url(image_url)
4691
4690
  return await self.send_image_file(chat_id, local_path, caption, reply_to, metadata)
4692
4691
  except Exception:
@@ -4694,7 +4693,6 @@ class InlineAdapter(BasePlatformAdapter):
4694
4693
 
4695
4694
  async def send_animation(self, chat_id: str, animation_url: str, caption: Optional[str] = None, reply_to: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> SendResult:
4696
4695
  try:
4697
- from gateway.platforms.base import cache_image_from_url
4698
4696
  local_path = await cache_image_from_url(animation_url, ext=".gif")
4699
4697
  return await self.send_document(chat_id, local_path, caption, file_name=Path(local_path).name, reply_to=reply_to, metadata=metadata)
4700
4698
  except Exception:
@@ -4982,7 +4980,7 @@ class InlineAdapter(BasePlatformAdapter):
4982
4980
  }
4983
4981
  actions: list[Dict[str, str]] = []
4984
4982
  try:
4985
- from hermes_cli.models import group_providers
4983
+ from hermes_cli.models_catalog_static import group_providers
4986
4984
  grouped = group_providers(list(by_slug.keys()))
4987
4985
  except Exception:
4988
4986
  grouped = [{"kind": "single", "slug": slug} for slug in by_slug.keys()]
@@ -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.18-alpha.0"
30
+ _PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.18"
31
31
  _ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
32
32
 
33
33
 
@@ -362,6 +362,7 @@ def register_cli(parser: argparse.ArgumentParser) -> None:
362
362
  setup.add_argument("--json", action="store_true", help="Print compact machine-readable setup output.")
363
363
  status = subs.add_parser("status", help="Show Inline adapter status", description="Check Inline configuration, sidecar, Node runtime, and optional credential identity.")
364
364
  status.add_argument("--json", action="store_true", help="Print compact machine-readable status output.")
365
+ status.add_argument("--check-compatibility", action="store_true", help="Validate the installed plugin with the Hermes loader.")
365
366
  status.add_argument("--probe", action="store_true", help="Verify the configured Inline credential and bot identity.")
366
367
  parser.set_defaults(func=dispatch)
367
368
 
@@ -453,7 +454,9 @@ def _status(args) -> int:
453
454
  and sidecar["size"] > 0
454
455
  )
455
456
  runtime_usable = bool(sidecar["ok"] and node["ok"])
456
- ready = runtime_usable and configured and (not probe_requested or bool(probe and probe.get("ok")))
457
+ compatibility = _compatibility_status() if getattr(args, "check_compatibility", False) else None
458
+ compatible = compatibility is None or compatibility["ok"]
459
+ ready = compatible and runtime_usable and configured and (not probe_requested or bool(probe and probe.get("ok")))
457
460
  result = {
458
461
  "ok": ready,
459
462
  "ready": ready,
@@ -467,11 +470,14 @@ def _status(args) -> int:
467
470
  "node": node,
468
471
  "probeRequested": probe_requested,
469
472
  "gateway": _gateway_status(),
473
+ **({"compatibility": compatibility} if compatibility is not None else {}),
470
474
  **({"probe": probe} if probe is not None else {}),
471
475
  }
472
476
  if getattr(args, "json", False):
473
477
  print(json.dumps(result, separators=(",", ":")))
474
478
  else:
479
+ if compatibility is not None:
480
+ print(f"Hermes plugin compatible: {'yes' if compatibility['ok'] else 'no'} ({compatibility['reason']})")
475
481
  print(f"Inline configured: {'yes' if configured else 'no'}")
476
482
  print(f"Inline sidecar usable: {'yes' if sidecar['ok'] else 'no'}")
477
483
  print(f"Node available: {_node_status_text(node)}")
@@ -481,7 +487,46 @@ def _status(args) -> int:
481
487
  elif probe_requested:
482
488
  print(f"Inline credential probe: {'ready' if ready else 'failed'}")
483
489
  print("Advanced diagnostics: inline-hermes doctor --json")
484
- return 0 if runtime_usable and (not probe_requested or ready) else 1
490
+ return 0 if compatible and runtime_usable and (not probe_requested or ready) else 1
491
+
492
+
493
+ def _compatibility_status() -> dict:
494
+ """Attest only a successful host load of this exact installed plugin.
495
+
496
+ Never return host exception text: configuration or import errors may contain
497
+ credentials. A missing diagnostic API is an inconclusive check, not success.
498
+ """
499
+ unavailable = {"ok": False, "reason": "loader_unavailable"}
500
+ try:
501
+ from hermes_cli.plugins import get_plugin_manager
502
+
503
+ manager = get_plugin_manager()
504
+ manager.discover_and_load()
505
+ plugin_dir = Path(__file__).resolve().parent
506
+ loaded = next((
507
+ item for item in manager._plugins.values()
508
+ if item.manifest.name == "inline-platform"
509
+ and Path(item.manifest.path).resolve() == plugin_dir
510
+ ), None)
511
+ if loaded is None:
512
+ return {"ok": False, "reason": "plugin_not_loaded"}
513
+ if not loaded.enabled or loaded.error or loaded.module is None:
514
+ return {"ok": False, "reason": "plugin_load_failed"}
515
+ if "inline" not in loaded.tools_registered:
516
+ return {"ok": False, "reason": "tool_not_registered"}
517
+ # Older supported Hermes releases predate the moved-import scanner.
518
+ # Their loader still checks real imports and plugin registration above.
519
+ try:
520
+ from hermes_cli.plugin_compat import plugin_hits
521
+ except ModuleNotFoundError as error:
522
+ if error.name != "hermes_cli.plugin_compat":
523
+ raise
524
+ else:
525
+ if plugin_hits(loaded.manifest):
526
+ return {"ok": False, "reason": "deprecated_imports"}
527
+ return {"ok": True, "reason": "loaded", "pluginPath": str(plugin_dir)}
528
+ except Exception:
529
+ return unavailable
485
530
 
486
531
 
487
532
  def _gateway_status() -> dict:
@@ -1,7 +1,10 @@
1
1
  name: inline-platform
2
2
  label: Inline
3
3
  kind: platform
4
- version: 0.0.18-alpha.0
4
+ provides_tools:
5
+ - inline
6
+ version: 0.0.18
7
+ requires_hermes: ">=0.21.4"
5
8
  description: >
6
9
  Inline platform adapter for Hermes Agent. The adapter runs as a native
7
10
  Hermes Python platform plugin and supervises a local Node sidecar that uses