hermes-plugin-tinyfish 0.2.4__tar.gz → 0.3.0__tar.gz

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.
Files changed (48) hide show
  1. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/CHANGELOG.md +19 -0
  2. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/PKG-INFO +58 -5
  3. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/README.md +56 -3
  4. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/__init__.py +13 -3
  5. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/config.py +12 -0
  6. hermes_plugin_tinyfish-0.3.0/hermes_plugin_tinyfish/routing_context.py +75 -0
  7. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/setup_cli.py +34 -8
  8. hermes_plugin_tinyfish-0.3.0/hermes_plugin_tinyfish/update_check.py +290 -0
  9. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/plugin.yaml +2 -2
  10. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/pyproject.toml +2 -2
  11. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_register.py +6 -1
  12. hermes_plugin_tinyfish-0.3.0/tests/test_routing_context.py +136 -0
  13. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_setup_cli.py +34 -3
  14. hermes_plugin_tinyfish-0.3.0/tests/test_update_check.py +336 -0
  15. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  16. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -0
  17. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/generate_report.py +0 -0
  18. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/improve_description.py +0 -0
  19. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/package_skill.py +0 -0
  20. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/quick_validate.py +0 -0
  21. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/run_eval.py +0 -0
  22. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/run_loop.py +0 -0
  23. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.agents/skills/skill-creator/scripts/utils.py +0 -0
  24. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/.gitignore +0 -0
  25. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  26. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/CONTRIBUTING.md +0 -0
  27. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/LICENSE +0 -0
  28. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/SECURITY.md +0 -0
  29. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/__init__.py +0 -0
  30. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/browser_provider.py +0 -0
  31. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/credit_policy.py +0 -0
  32. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/health.py +0 -0
  33. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/normalize.py +0 -0
  34. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/provider.py +0 -0
  35. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/hermes_plugin_tinyfish/rest_client.py +0 -0
  36. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/scripts/prepare_release.py +0 -0
  37. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/scripts/validate_release.py +0 -0
  38. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/conftest.py +0 -0
  39. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_browser_provider.py +0 -0
  40. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_credit_policy.py +0 -0
  41. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_health.py +0 -0
  42. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_live.py +0 -0
  43. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_normalize.py +0 -0
  44. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_prepare_release.py +0 -0
  45. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_provider.py +0 -0
  46. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_rest_client.py +0 -0
  47. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_validate_release.py +0 -0
  48. {hermes_plugin_tinyfish-0.2.4 → hermes_plugin_tinyfish-0.3.0}/tests/test_version.py +0 -0
@@ -7,6 +7,25 @@ and this project uses semantic versioning.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## [0.3.0] - 2026-08-04
11
+
12
+ ### Added
13
+
14
+ - Added marked, once-per-active-context guidance so Hermes can infer when
15
+ ordinary web requests should use generic provider tools and when
16
+ TinyFish-specific controls require native MCP Search or Fetch without
17
+ accumulating one new copy on every turn.
18
+ - Added nonblocking, source-aware update checks with a 24-hour profile-local
19
+ cache, one-time conversational notices, install-appropriate update guidance,
20
+ opt-outs, and non-networked diagnostics.
21
+
22
+ ### Changed
23
+
24
+ - Expanded diagnostics to schema version 3 with routing state, installed and
25
+ cached-latest versions, release channel, and update availability.
26
+ - Clarified that persistent Search/Fetch settings are REST fallback defaults,
27
+ not required parity configuration for plain-language requests.
28
+
10
29
  ## [0.2.4] - 2026-07-22
11
30
 
12
31
  ### Fixed
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hermes-plugin-tinyfish
3
- Version: 0.2.4
4
- Summary: TinyFish Search, Fetch, and optional Browser provider plugin for Hermes Agent
3
+ Version: 0.3.0
4
+ Summary: TinyFish Search, Fetch, plain-language routing, update awareness, and optional Browser for Hermes Agent
5
5
  Project-URL: Homepage, https://github.com/gabeosx/hermes-plugin-tinyfish
6
6
  Project-URL: Documentation, https://github.com/gabeosx/hermes-plugin-tinyfish#readme
7
7
  Project-URL: Issues, https://github.com/gabeosx/hermes-plugin-tinyfish/issues
@@ -38,8 +38,8 @@ Description-Content-Type: text/markdown
38
38
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
39
39
  [![Security Policy](https://img.shields.io/badge/Security-policy-green.svg)](SECURITY.md)
40
40
 
41
- TinyFish Search and Fetch providers, plus optional TinyFish Browser
42
- infrastructure, for
41
+ TinyFish Search and Fetch providers with plain-language tool routing, update
42
+ awareness, and optional TinyFish Browser infrastructure for
43
43
  [Hermes Agent](https://hermes-agent.nousresearch.com/docs/).
44
44
 
45
45
  Hermes remains the agent: it plans, chooses tools, and controls browser
@@ -88,6 +88,20 @@ hermes tinyfish status
88
88
  hermes tinyfish doctor
89
89
  ```
90
90
 
91
+ The plugin also performs a nonblocking update check when it loads. Git installs
92
+ are compared with the latest stable GitHub Release; Python package installs are
93
+ compared with the latest stable PyPI version. Results are cached for 24 hours
94
+ at `$HERMES_HOME/cache/web-tinyfish-update.json` in the active profile. If an
95
+ update is available, Hermes mentions it once on the first conversational turn
96
+ where the cached or background result is available. The plugin never installs
97
+ the update itself.
98
+
99
+ The check sends only the installed plugin version in its HTTP User-Agent. It
100
+ does not send Hermes configuration, TinyFish credentials, searches, fetched
101
+ URLs, or conversation content. Network and cache failures are silent. Disable
102
+ the check with `tinyfish.update_check: false` or the conventional
103
+ `NO_UPDATE_NOTIFIER=1` environment variable.
104
+
91
105
  ## Safe Default Setup
92
106
 
93
107
  `hermes tinyfish setup` configures TinyFish for Hermes `web_search` and
@@ -108,6 +122,8 @@ web:
108
122
  extract_backend: tinyfish
109
123
 
110
124
  tinyfish:
125
+ routing_context: true
126
+ update_check: true
111
127
  credit_policy:
112
128
  browser: deny
113
129
  ```
@@ -165,6 +181,35 @@ For browser authorization from a headless or remote terminal:
165
181
  - Treat the MCP-only doctor as authoritative. Some Hermes versions can print
166
182
  an authentication failure while still returning shell status 0.
167
183
 
184
+ ## Automatic Tool Routing
185
+
186
+ Talk to Hermes normally; you do not need to choose “the plugin” or “MCP.” When
187
+ an active context does not already contain the current routing version, the
188
+ plugin adds a short routing note that helps Hermes choose the narrowest tool
189
+ surface:
190
+
191
+ - “Find recent TinyFish documentation” uses Hermes `web_search`.
192
+ - “Read this page and summarize it” uses Hermes `web_extract`.
193
+ - “Search only these domains, in English, after this date, and return page 2”
194
+ uses TinyFish MCP's native `search` tool because the generic search schema
195
+ cannot express all of those controls.
196
+ - “Fetch these URLs using this selector, include image links, and use this
197
+ cache TTL” uses TinyFish MCP's native `fetch_content` tool.
198
+
199
+ Hermes keeps the visible stored user message clean but persists the exact
200
+ API-bound message in an `api_content` sidecar for prompt-cache replay. The note
201
+ contains a versioned marker, so the hook does not add another copy while that
202
+ version remains in the active context. If compression removes the marked
203
+ message, the hook adds one fresh copy. Multiple tool calls inside a turn do not
204
+ run the hook again. Hermes still sees the real tool schemas and makes the final
205
+ choice; the plugin does not keyword-match the prompt or force a tool.
206
+
207
+ Routing guidance defaults on only when the plugin-managed TinyFish MCP server
208
+ is configured. Set `tinyfish.routing_context: false` to disable the guidance
209
+ without disabling the provider. This prevents new injections; a copy already
210
+ present in the active API context may remain until that context is reset or
211
+ compressed.
212
+
168
213
  ## Search and Fetch Options
169
214
 
170
215
  Optional REST fallback defaults can be configured in `config.yaml`:
@@ -185,7 +230,10 @@ tinyfish:
185
230
  ```
186
231
 
187
232
  MCP remains the preferred path when configured. These options apply only to
188
- REST fallback calls.
233
+ REST fallback calls. They are fixed fallback defaults, not required settings
234
+ for ordinary requests and not a substitute for plain-language, per-request MCP
235
+ controls. The plugin does not add persistent include-domain, exclude-selector,
236
+ or similar parity settings.
189
237
 
190
238
  ## Optional Browser Provider
191
239
 
@@ -243,6 +291,9 @@ hermes tinyfish doctor --live-paid
243
291
 
244
292
  - `status` is non-secret and reports ignored `0.2.x` policy keys under
245
293
  `retired_credit_policy_keys`.
294
+ - Status also reports whether routing and update checks are enabled, whether
295
+ routing is active, the installed version, the cached latest version, and
296
+ cached update availability. Reading status never performs an update request.
246
297
  - `mcp_token_cached` reports only whether Hermes's expected cache file is
247
298
  present. It does not mean the access or refresh token is valid.
248
299
  - `/tinyfish-status` shows non-networked status inside CLI or gateway sessions;
@@ -263,6 +314,8 @@ This plugin uses public Hermes extension points:
263
314
  - `ctx.register_web_search_provider(...)`
264
315
  - `ctx.register_browser_provider(...)`
265
316
  - `ctx.register_hook("pre_tool_call", ...)` for Browser credit policy
317
+ - `ctx.register_hook("pre_llm_call", ...)` for context-aware tool routing and
318
+ update notices
266
319
  - `ctx.register_cli_command(...)`
267
320
  - `ctx.register_command(...)` for `/tinyfish-status`
268
321
  - `ctx.dispatch_tool(...)` for registered TinyFish MCP calls
@@ -6,8 +6,8 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
7
  [![Security Policy](https://img.shields.io/badge/Security-policy-green.svg)](SECURITY.md)
8
8
 
9
- TinyFish Search and Fetch providers, plus optional TinyFish Browser
10
- infrastructure, for
9
+ TinyFish Search and Fetch providers with plain-language tool routing, update
10
+ awareness, and optional TinyFish Browser infrastructure for
11
11
  [Hermes Agent](https://hermes-agent.nousresearch.com/docs/).
12
12
 
13
13
  Hermes remains the agent: it plans, chooses tools, and controls browser
@@ -56,6 +56,20 @@ hermes tinyfish status
56
56
  hermes tinyfish doctor
57
57
  ```
58
58
 
59
+ The plugin also performs a nonblocking update check when it loads. Git installs
60
+ are compared with the latest stable GitHub Release; Python package installs are
61
+ compared with the latest stable PyPI version. Results are cached for 24 hours
62
+ at `$HERMES_HOME/cache/web-tinyfish-update.json` in the active profile. If an
63
+ update is available, Hermes mentions it once on the first conversational turn
64
+ where the cached or background result is available. The plugin never installs
65
+ the update itself.
66
+
67
+ The check sends only the installed plugin version in its HTTP User-Agent. It
68
+ does not send Hermes configuration, TinyFish credentials, searches, fetched
69
+ URLs, or conversation content. Network and cache failures are silent. Disable
70
+ the check with `tinyfish.update_check: false` or the conventional
71
+ `NO_UPDATE_NOTIFIER=1` environment variable.
72
+
59
73
  ## Safe Default Setup
60
74
 
61
75
  `hermes tinyfish setup` configures TinyFish for Hermes `web_search` and
@@ -76,6 +90,8 @@ web:
76
90
  extract_backend: tinyfish
77
91
 
78
92
  tinyfish:
93
+ routing_context: true
94
+ update_check: true
79
95
  credit_policy:
80
96
  browser: deny
81
97
  ```
@@ -133,6 +149,35 @@ For browser authorization from a headless or remote terminal:
133
149
  - Treat the MCP-only doctor as authoritative. Some Hermes versions can print
134
150
  an authentication failure while still returning shell status 0.
135
151
 
152
+ ## Automatic Tool Routing
153
+
154
+ Talk to Hermes normally; you do not need to choose “the plugin” or “MCP.” When
155
+ an active context does not already contain the current routing version, the
156
+ plugin adds a short routing note that helps Hermes choose the narrowest tool
157
+ surface:
158
+
159
+ - “Find recent TinyFish documentation” uses Hermes `web_search`.
160
+ - “Read this page and summarize it” uses Hermes `web_extract`.
161
+ - “Search only these domains, in English, after this date, and return page 2”
162
+ uses TinyFish MCP's native `search` tool because the generic search schema
163
+ cannot express all of those controls.
164
+ - “Fetch these URLs using this selector, include image links, and use this
165
+ cache TTL” uses TinyFish MCP's native `fetch_content` tool.
166
+
167
+ Hermes keeps the visible stored user message clean but persists the exact
168
+ API-bound message in an `api_content` sidecar for prompt-cache replay. The note
169
+ contains a versioned marker, so the hook does not add another copy while that
170
+ version remains in the active context. If compression removes the marked
171
+ message, the hook adds one fresh copy. Multiple tool calls inside a turn do not
172
+ run the hook again. Hermes still sees the real tool schemas and makes the final
173
+ choice; the plugin does not keyword-match the prompt or force a tool.
174
+
175
+ Routing guidance defaults on only when the plugin-managed TinyFish MCP server
176
+ is configured. Set `tinyfish.routing_context: false` to disable the guidance
177
+ without disabling the provider. This prevents new injections; a copy already
178
+ present in the active API context may remain until that context is reset or
179
+ compressed.
180
+
136
181
  ## Search and Fetch Options
137
182
 
138
183
  Optional REST fallback defaults can be configured in `config.yaml`:
@@ -153,7 +198,10 @@ tinyfish:
153
198
  ```
154
199
 
155
200
  MCP remains the preferred path when configured. These options apply only to
156
- REST fallback calls.
201
+ REST fallback calls. They are fixed fallback defaults, not required settings
202
+ for ordinary requests and not a substitute for plain-language, per-request MCP
203
+ controls. The plugin does not add persistent include-domain, exclude-selector,
204
+ or similar parity settings.
157
205
 
158
206
  ## Optional Browser Provider
159
207
 
@@ -211,6 +259,9 @@ hermes tinyfish doctor --live-paid
211
259
 
212
260
  - `status` is non-secret and reports ignored `0.2.x` policy keys under
213
261
  `retired_credit_policy_keys`.
262
+ - Status also reports whether routing and update checks are enabled, whether
263
+ routing is active, the installed version, the cached latest version, and
264
+ cached update availability. Reading status never performs an update request.
214
265
  - `mcp_token_cached` reports only whether Hermes's expected cache file is
215
266
  present. It does not mean the access or refresh token is valid.
216
267
  - `/tinyfish-status` shows non-networked status inside CLI or gateway sessions;
@@ -231,6 +282,8 @@ This plugin uses public Hermes extension points:
231
282
  - `ctx.register_web_search_provider(...)`
232
283
  - `ctx.register_browser_provider(...)`
233
284
  - `ctx.register_hook("pre_tool_call", ...)` for Browser credit policy
285
+ - `ctx.register_hook("pre_llm_call", ...)` for context-aware tool routing and
286
+ update notices
234
287
  - `ctx.register_cli_command(...)`
235
288
  - `ctx.register_command(...)` for `/tinyfish-status`
236
289
  - `ctx.dispatch_tool(...)` for registered TinyFish MCP calls
@@ -39,22 +39,28 @@ __version__ = _resolve_version()
39
39
 
40
40
 
41
41
  def register(ctx: Any) -> None:
42
- """Register TinyFish providers, Browser policy hook, and CLI commands."""
42
+ """Register TinyFish providers, lifecycle hooks, and CLI commands."""
43
43
 
44
44
  from .browser_provider import TinyFishBrowserProvider
45
45
  from .credit_policy import pre_tool_call_policy
46
46
  from .provider import TinyFishWebSearchProvider
47
+ from .routing_context import TinyFishTurnContext
47
48
  from .setup_cli import dispatch_tinyfish_cli, setup_tinyfish_cli, tinyfish_status_command
49
+ from .update_check import UpdateChecker, resolve_install_info
48
50
 
49
51
  provider = TinyFishWebSearchProvider(dispatch_tool=getattr(ctx, "dispatch_tool", None))
52
+ update_checker = UpdateChecker(resolve_install_info(ctx, __version__))
53
+ turn_context = TinyFishTurnContext(update_checker)
50
54
  ctx.register_web_search_provider(provider)
51
55
  if hasattr(ctx, "register_browser_provider"):
52
56
  ctx.register_browser_provider(TinyFishBrowserProvider())
53
57
  if hasattr(ctx, "register_hook"):
54
58
  ctx.register_hook("pre_tool_call", pre_tool_call_policy)
59
+ ctx.register_hook("pre_llm_call", turn_context)
60
+ update_checker.start()
55
61
 
56
62
  def _dispatch_cli(args: Any) -> int:
57
- exit_code = dispatch_tinyfish_cli(args, provider=provider)
63
+ exit_code = dispatch_tinyfish_cli(args, provider=provider, update_checker=update_checker)
58
64
  if exit_code:
59
65
  raise SystemExit(exit_code)
60
66
  return 0
@@ -69,7 +75,11 @@ def register(ctx: Any) -> None:
69
75
  if hasattr(ctx, "register_command"):
70
76
  ctx.register_command(
71
77
  name="tinyfish-status",
72
- handler=lambda raw_args: tinyfish_status_command(raw_args, provider=provider),
78
+ handler=lambda raw_args: tinyfish_status_command(
79
+ raw_args,
80
+ provider=provider,
81
+ update_checker=update_checker,
82
+ ),
73
83
  description="Show TinyFish provider status; pass 'live' to test Search and Fetch.",
74
84
  args_hint="[live]",
75
85
  )
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any, Literal
6
6
 
7
+ TINYFISH_MCP_URL = "https://agent.tinyfish.ai/mcp"
8
+
7
9
  CreditFeature = Literal["browser"]
8
10
  CreditPolicy = Literal["deny", "request", "allow"]
9
11
 
@@ -58,6 +60,16 @@ def tinyfish_config(config: dict[str, Any] | None = None) -> dict[str, Any]:
58
60
  return section if isinstance(section, dict) else {}
59
61
 
60
62
 
63
+ def routing_context_enabled(config: dict[str, Any] | None = None) -> bool:
64
+ value = _bool_option(tinyfish_config(config).get("routing_context"))
65
+ return True if value is None else value
66
+
67
+
68
+ def update_check_enabled(config: dict[str, Any] | None = None) -> bool:
69
+ value = _bool_option(tinyfish_config(config).get("update_check"))
70
+ return True if value is None else value
71
+
72
+
61
73
  def credit_policy(feature: CreditFeature | str, config: dict[str, Any] | None = None) -> CreditPolicy:
62
74
  normalized = normalize_feature(str(feature))
63
75
  section = tinyfish_config(config)
@@ -0,0 +1,75 @@
1
+ """Context-bounded TinyFish tool-routing guidance for Hermes turns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ from .config import TINYFISH_MCP_URL, load_config, routing_context_enabled
9
+ from .update_check import UpdateChecker
10
+
11
+ ROUTING_CONTEXT_MARKER = '<tinyfish-routing-context version="1">'
12
+ ROUTING_GUIDANCE = f"""{ROUTING_CONTEXT_MARKER}
13
+ TinyFish tool-routing guidance:
14
+ - For ordinary web discovery or reading a page, use Hermes `web_search` or `web_extract`; the TinyFish provider already keeps those calls MCP-first.
15
+ - When the request needs TinyFish-specific controls that the generic schemas cannot express—domain/date/language/location/purpose/pagination filters, selectors, output formats, link or image extraction, cache TTL, or per-URL timeouts—use the native `search` or `fetch_content` tool registered by the `tinyfish` MCP server. Use the exact names Hermes exposes, commonly `mcp__tinyfish__search` and `mcp__tinyfish__fetch_content`.
16
+ - Infer the choice from the user's plain language. Do not ask them to choose MCP versus the plugin, and do not persist per-request controls as configuration."""
17
+
18
+
19
+ def tinyfish_mcp_configured(config: dict[str, Any]) -> bool:
20
+ servers = config.get("mcp_servers") or {}
21
+ if not isinstance(servers, dict):
22
+ return False
23
+ tinyfish = servers.get("tinyfish") or {}
24
+ return bool(
25
+ isinstance(tinyfish, dict)
26
+ and tinyfish.get("url") == TINYFISH_MCP_URL
27
+ and tinyfish.get("auth") == "oauth"
28
+ )
29
+
30
+
31
+ def _contains_routing_marker(value: object) -> bool:
32
+ if isinstance(value, str):
33
+ return ROUTING_CONTEXT_MARKER in value
34
+ if isinstance(value, Mapping):
35
+ return any(_contains_routing_marker(item) for item in value.values())
36
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
37
+ return any(_contains_routing_marker(item) for item in value)
38
+ return False
39
+
40
+
41
+ def routing_guidance_present(conversation_history: object) -> bool:
42
+ """Return whether Hermes already carries this routing version in context."""
43
+
44
+ if not isinstance(conversation_history, Sequence) or isinstance(
45
+ conversation_history, (str, bytes, bytearray)
46
+ ):
47
+ return False
48
+ for message in conversation_history:
49
+ if not isinstance(message, Mapping):
50
+ continue
51
+ if _contains_routing_marker(message.get("api_content")):
52
+ return True
53
+ if _contains_routing_marker(message.get("content")):
54
+ return True
55
+ return False
56
+
57
+
58
+ class TinyFishTurnContext:
59
+ """Compose once-per-active-context routing with a maintenance notice."""
60
+
61
+ def __init__(self, update_checker: UpdateChecker) -> None:
62
+ self.update_checker = update_checker
63
+
64
+ def __call__(self, **kwargs: Any) -> dict[str, str] | None:
65
+ config = load_config()
66
+ context: list[str] = []
67
+ routing_needed = not routing_guidance_present(kwargs.get("conversation_history"))
68
+ if routing_context_enabled(config) and tinyfish_mcp_configured(config) and routing_needed:
69
+ context.append(ROUTING_GUIDANCE)
70
+ update_notice = self.update_checker.notice_context()
71
+ if update_notice:
72
+ context.append(update_notice)
73
+ if not context:
74
+ return None
75
+ return {"context": "\n\n".join(context)}
@@ -14,11 +14,13 @@ from .config import (
14
14
  CREDIT_FEATURES,
15
15
  CREDIT_POLICIES,
16
16
  RETIRED_CREDIT_POLICY_KEYS,
17
+ TINYFISH_MCP_URL,
17
18
  credit_policy_summary,
18
19
  normalize_feature,
19
20
  normalize_policy,
20
21
  reset_credit_policies,
21
22
  retired_credit_policy_keys,
23
+ routing_context_enabled,
22
24
  set_credit_policy,
23
25
  )
24
26
  from .credit_policy import INDEPENDENT_NOTICE, PRICING_NOTICE, request_credit_approval
@@ -28,8 +30,9 @@ from .provider import (
28
30
  MCP_SEARCH_TOOLS,
29
31
  TinyFishWebSearchProvider,
30
32
  )
33
+ from .routing_context import tinyfish_mcp_configured
34
+ from .update_check import InstallInfo, UpdateChecker
31
35
 
32
- TINYFISH_MCP_URL = "https://agent.tinyfish.ai/mcp"
33
36
  MCP_LOGIN_COMMAND = ("hermes", "mcp", "login", "tinyfish")
34
37
 
35
38
 
@@ -90,14 +93,15 @@ def dispatch_tinyfish_cli(
90
93
  args: argparse.Namespace,
91
94
  *,
92
95
  provider: TinyFishWebSearchProvider | None = None,
96
+ update_checker: UpdateChecker | None = None,
93
97
  ) -> int:
94
98
  command = getattr(args, "tinyfish_command", None) or "status"
95
99
  if command == "setup":
96
- return cmd_setup(args, provider=provider)
100
+ return cmd_setup(args, provider=provider, update_checker=update_checker)
97
101
  if command == "doctor":
98
- return cmd_doctor(args, provider=provider)
102
+ return cmd_doctor(args, provider=provider, update_checker=update_checker)
99
103
  if command == "status":
100
- return cmd_status(args, provider=provider)
104
+ return cmd_status(args, provider=provider, update_checker=update_checker)
101
105
  if command == "reauth":
102
106
  return cmd_reauth(args)
103
107
  if command == "credits":
@@ -207,6 +211,7 @@ def cmd_setup(
207
211
  args: argparse.Namespace,
208
212
  *,
209
213
  provider: TinyFishWebSearchProvider | None = None,
214
+ update_checker: UpdateChecker | None = None,
210
215
  ) -> int:
211
216
  config = _load_config()
212
217
 
@@ -254,7 +259,7 @@ def cmd_setup(
254
259
 
255
260
  if getattr(args, "live", False):
256
261
  doctor_args = argparse.Namespace(json=False, live=True, live_paid=False, transport="auto")
257
- return cmd_doctor(doctor_args, provider=provider)
262
+ return cmd_doctor(doctor_args, provider=provider, update_checker=update_checker)
258
263
 
259
264
  print("Run `hermes tinyfish doctor --live` to verify the setup.")
260
265
  browser_policy = credit_policy_summary(config)["browser"]
@@ -346,6 +351,7 @@ def collect_status(
346
351
  live: bool = False,
347
352
  transport: Transport = "auto",
348
353
  provider: TinyFishWebSearchProvider | None = None,
354
+ update_checker: UpdateChecker | None = None,
349
355
  ) -> dict[str, Any]:
350
356
  config = _load_config()
351
357
  mcp_cfg = (config.get("mcp_servers") or {}).get("tinyfish") or {}
@@ -358,9 +364,17 @@ def collect_status(
358
364
  api_key_configured = bool(_get_env("TINYFISH_API_KEY"))
359
365
  provider = provider or TinyFishWebSearchProvider()
360
366
  health = _health_status(provider)
367
+ if update_checker is None:
368
+ from . import __version__
369
+
370
+ update_checker = UpdateChecker(
371
+ InstallInfo(current_version=__version__, channel="github", update_command=None),
372
+ home=_hermes_home(),
373
+ )
374
+ update_status = update_checker.status(config)
361
375
 
362
376
  checks: dict[str, Any] = {
363
- "diagnostics_schema_version": 2,
377
+ "diagnostics_schema_version": 3,
364
378
  "plugin_loaded": True,
365
379
  "provider_available": provider.is_available(),
366
380
  "mcp_configured": bool(mcp_cfg.get("url") == TINYFISH_MCP_URL and mcp_cfg.get("auth") == "oauth"),
@@ -371,6 +385,8 @@ def collect_status(
371
385
  "api_key_fallback_configured": api_key_configured,
372
386
  "credit_policy": credit_policy_summary(config),
373
387
  "retired_credit_policy_keys": retired_credit_policy_keys(config),
388
+ "routing_context_enabled": routing_context_enabled(config),
389
+ "routing_context_active": bool(routing_context_enabled(config) and tinyfish_mcp_configured(config)),
374
390
  "independent_plugin_notice": INDEPENDENT_NOTICE,
375
391
  "pricing_notice": PRICING_NOTICE,
376
392
  "web_search_backend": web_cfg.get("search_backend") or web_cfg.get("backend") or "",
@@ -378,6 +394,7 @@ def collect_status(
378
394
  "browser_cloud_provider": (config.get("browser") or {}).get("cloud_provider", "")
379
395
  if isinstance(config.get("browser") or {}, dict)
380
396
  else "",
397
+ **update_status,
381
398
  **health,
382
399
  }
383
400
 
@@ -483,8 +500,9 @@ def cmd_status(
483
500
  args: argparse.Namespace,
484
501
  *,
485
502
  provider: TinyFishWebSearchProvider | None = None,
503
+ update_checker: UpdateChecker | None = None,
486
504
  ) -> int:
487
- status = collect_status(live=False, provider=provider)
505
+ status = collect_status(live=False, provider=provider, update_checker=update_checker)
488
506
  if getattr(args, "json", False):
489
507
  print(json.dumps(status, indent=2, sort_keys=True))
490
508
  else:
@@ -496,6 +514,7 @@ def cmd_doctor(
496
514
  args: argparse.Namespace,
497
515
  *,
498
516
  provider: TinyFishWebSearchProvider | None = None,
517
+ update_checker: UpdateChecker | None = None,
499
518
  ) -> int:
500
519
  live = bool(getattr(args, "live", False))
501
520
  transport = str(getattr(args, "transport", "auto") or "auto")
@@ -510,6 +529,7 @@ def cmd_doctor(
510
529
  live=live,
511
530
  transport=cast(Transport, transport),
512
531
  provider=provider,
532
+ update_checker=update_checker,
513
533
  )
514
534
  if getattr(args, "live_paid", False):
515
535
  paid_ok = _run_live_paid_checks(status)
@@ -566,13 +586,19 @@ def tinyfish_status_command(
566
586
  raw_args: str,
567
587
  *,
568
588
  provider: TinyFishWebSearchProvider,
589
+ update_checker: UpdateChecker | None = None,
569
590
  ) -> str:
570
591
  """Serve the in-session ``/tinyfish-status`` command."""
571
592
 
572
593
  option = (raw_args or "").strip().lower()
573
594
  if option not in {"", "live"}:
574
595
  return "Usage: /tinyfish-status [live]"
575
- status = collect_status(live=option == "live", transport="auto", provider=provider)
596
+ status = collect_status(
597
+ live=option == "live",
598
+ transport="auto",
599
+ provider=provider,
600
+ update_checker=update_checker,
601
+ )
576
602
  return "\n".join(_status_lines(status))
577
603
 
578
604