know-fast 0.1.0__py3-none-any.whl

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.
know_fast/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ """know-fast — Hermes plugin for ⚡️ know.fast: links in, knowledge out.
2
+
3
+ Send any link (article, YouTube, X post, podcast, paper, PDF) and get back a
4
+ token-efficient knowledge packet plus a durable knowledge page. Links are
5
+ unique — anything anyone has ever known answers instantly. Flat $0.01 per call.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ logger = logging.getLogger("know_fast")
15
+
16
+
17
+ def register(ctx) -> None:
18
+ """Hermes plugin entry point (entry point group: hermes_agent.plugins)."""
19
+ from . import provider as provider_module
20
+
21
+ if not provider_module.HERMES_AVAILABLE:
22
+ logger.warning(
23
+ "know-fast: hermes-agent is not importable (module 'agent' not "
24
+ "found); skipping registration. Install and run inside hermes-agent "
25
+ "to use this plugin."
26
+ )
27
+ return
28
+
29
+ ctx.register_web_search_provider(provider_module.KnowFastProvider())
30
+
31
+ from . import tools
32
+
33
+ tools.register_tools(ctx)
34
+ logger.info("know-fast: registered web provider and %d tools", len(tools.TOOLS))
know_fast/client.py ADDED
@@ -0,0 +1,149 @@
1
+ """Tiny stdlib-only JSON client for the know.fast API.
2
+
3
+ No third-party dependencies: urllib + json only.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import os
10
+ import socket
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+
15
+ DEFAULT_BASE_URL = "https://know.fast"
16
+ CLIENT_HEADER = "knowfast-hermes-pip"
17
+ PLUGIN_VERSION = "0.1.0"
18
+ KEY_HINT = (
19
+ "Invalid or missing API key. know.fast is a flat $0.01 per link: a "
20
+ "wallet-native agent can pay keylessly — any POST with no key returns a "
21
+ "402 x402/MPP challenge; authorize it and the first paid call returns a "
22
+ "durable key in the X-Researcher-Agent-Key header. Store that key and "
23
+ "set it as the KNOW_FAST_API_KEY environment variable."
24
+ )
25
+
26
+
27
+ class KnowFastError(Exception):
28
+ """API or transport failure. Carries HTTP status (if any) and parsed body."""
29
+
30
+ def __init__(self, message: str, status: int | None = None, body: dict | None = None):
31
+ super().__init__(message)
32
+ self.status = status
33
+ self.body = body if isinstance(body, dict) else {}
34
+
35
+
36
+ def base_url() -> str:
37
+ return os.getenv("KNOW_FAST_API_URL", DEFAULT_BASE_URL).rstrip("/")
38
+
39
+
40
+ def api_key() -> str:
41
+ # know.fast runs on the researcher.now account system — the same rk_ key
42
+ # works on both, so fall back to an already-configured researcher key.
43
+ return os.getenv("KNOW_FAST_API_KEY") or os.getenv("RESEARCHER_API_KEY", "")
44
+
45
+
46
+ def _parse_body(raw: bytes) -> dict:
47
+ try:
48
+ parsed = json.loads(raw.decode("utf-8", errors="replace"))
49
+ return parsed if isinstance(parsed, dict) else {"data": parsed}
50
+ except (ValueError, AttributeError):
51
+ return {}
52
+
53
+
54
+ def _apply_contract_headers(payload: dict, headers) -> dict:
55
+ """Surface the API's contract stamp to the agent.
56
+
57
+ The server stamps every response with X-Researcher-Contract (the agent
58
+ contract version — agents re-read agent.txt when it changes) and
59
+ X-Researcher-Notice (time-boxed change notices). X-Researcher-Min-Plugin
60
+ is deliberately ignored here: it versions the researcher-now package's
61
+ release stream, not know-fast's.
62
+ """
63
+ contract = headers.get("X-Researcher-Contract")
64
+ if contract:
65
+ payload.setdefault("contractVersion", contract)
66
+ notice = headers.get("X-Researcher-Notice")
67
+ if notice:
68
+ payload.setdefault("contractNotice", notice)
69
+ return payload
70
+
71
+
72
+ def _headers(body: dict | None) -> dict:
73
+ headers = {
74
+ "Accept": "application/json",
75
+ "X-Researcher-Client": CLIENT_HEADER,
76
+ }
77
+ key = api_key()
78
+ if key:
79
+ headers["X-Researcher-API-Key"] = key
80
+ headers["Authorization"] = "Bearer " + key
81
+ if body is not None:
82
+ headers["Content-Type"] = "application/json"
83
+ return headers
84
+
85
+
86
+ def request(
87
+ method: str,
88
+ path: str,
89
+ body: dict | None = None,
90
+ params: dict | None = None,
91
+ timeout: float = 30,
92
+ ) -> tuple[int, dict]:
93
+ """Make a JSON request. Returns (status, payload) on 2xx.
94
+
95
+ Raises KnowFastError on HTTP errors, timeouts, and network failures.
96
+ """
97
+ url = base_url() + path
98
+ if params:
99
+ url += "?" + urllib.parse.urlencode(params)
100
+ data = json.dumps(body).encode("utf-8") if body is not None else None
101
+ req = urllib.request.Request(url, data=data, headers=_headers(body), method=method)
102
+ try:
103
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
104
+ return resp.status, _apply_contract_headers(_parse_body(resp.read()), resp.headers)
105
+ except urllib.error.HTTPError as exc:
106
+ payload = _parse_body(exc.read())
107
+ message = str(payload.get("error") or payload.get("message") or "")
108
+ if exc.code == 401:
109
+ message = KEY_HINT if not message else f"{message} {KEY_HINT}"
110
+ elif not message:
111
+ message = f"know.fast API returned HTTP {exc.code}"
112
+ raise KnowFastError(message, status=exc.code, body=payload) from exc
113
+ except (TimeoutError, socket.timeout) as exc:
114
+ raise KnowFastError(f"Request to {url} timed out after {timeout:.0f}s") from exc
115
+ except urllib.error.URLError as exc:
116
+ reason = getattr(exc, "reason", exc)
117
+ if isinstance(reason, (TimeoutError, socket.timeout)):
118
+ raise KnowFastError(f"Request to {url} timed out after {timeout:.0f}s") from exc
119
+ raise KnowFastError(f"Network error reaching {url}: {reason}") from exc
120
+
121
+
122
+ def request_text(path: str, timeout: float = 30) -> str:
123
+ """GET a text resource (the <slug>.md knowledge markdown)."""
124
+ url = base_url() + path
125
+ headers = {
126
+ "Accept": "text/markdown, text/plain, application/json",
127
+ "X-Researcher-Client": CLIENT_HEADER,
128
+ }
129
+ key = api_key()
130
+ if key:
131
+ headers["X-Researcher-API-Key"] = key
132
+ headers["Authorization"] = "Bearer " + key
133
+ req = urllib.request.Request(url, headers=headers, method="GET")
134
+ try:
135
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
136
+ return resp.read().decode("utf-8", errors="replace")
137
+ except urllib.error.HTTPError as exc:
138
+ payload = _parse_body(exc.read())
139
+ message = str(payload.get("error") or payload.get("message") or "")
140
+ if not message:
141
+ message = f"know.fast API returned HTTP {exc.code}"
142
+ raise KnowFastError(message, status=exc.code, body=payload) from exc
143
+ except (TimeoutError, socket.timeout) as exc:
144
+ raise KnowFastError(f"Request to {url} timed out after {timeout:.0f}s") from exc
145
+ except urllib.error.URLError as exc:
146
+ reason = getattr(exc, "reason", exc)
147
+ if isinstance(reason, (TimeoutError, socket.timeout)):
148
+ raise KnowFastError(f"Request to {url} timed out after {timeout:.0f}s") from exc
149
+ raise KnowFastError(f"Network error reaching {url}: {reason}") from exc
know_fast/provider.py ADDED
@@ -0,0 +1,109 @@
1
+ """Hermes WebSearchProvider backed by know.fast — links in, knowledge out.
2
+
3
+ Extract-only provider: no search, but extract() returns the know.fast
4
+ knowledge packet (or full knowledge markdown) for each URL.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+
12
+ from . import client, tools
13
+
14
+ logger = logging.getLogger("know_fast")
15
+
16
+ try: # Hermes installs `agent` as a top-level module.
17
+ from agent.web_search_provider import WebSearchProvider
18
+
19
+ HERMES_AVAILABLE = True
20
+ except Exception: # pragma: no cover — exercised only outside Hermes
21
+ HERMES_AVAILABLE = False
22
+
23
+ class WebSearchProvider: # type: ignore[no-redef]
24
+ """Stub base class so this package imports standalone (tests, CI)."""
25
+
26
+
27
+ class KnowFastProvider(WebSearchProvider):
28
+ @property
29
+ def name(self) -> str:
30
+ return "know-fast"
31
+
32
+ @property
33
+ def display_name(self) -> str:
34
+ return "⚡️ know.fast"
35
+
36
+ def is_available(self) -> bool:
37
+ return bool(client.api_key())
38
+
39
+ def supports_search(self) -> bool:
40
+ return False
41
+
42
+ def supports_extract(self) -> bool:
43
+ return True
44
+
45
+ def extract(self, urls, **kwargs):
46
+ if isinstance(urls, str):
47
+ urls = [urls]
48
+ try:
49
+ urls = list(urls)
50
+ except TypeError:
51
+ return {"success": False, "error": f"extract() expects a list of URLs, got {type(urls).__name__}"}
52
+ if not self.is_available():
53
+ return {"success": False, "error": "KNOW_FAST_API_KEY is not set. " + client.KEY_HINT}
54
+
55
+ results = []
56
+ for url in urls:
57
+ entry = {
58
+ "url": url,
59
+ "title": "",
60
+ "content": "",
61
+ "raw_content": "",
62
+ "metadata": {},
63
+ }
64
+ try:
65
+ payload = json.loads(tools.know({"url": url}))
66
+ if payload.get("success") is False:
67
+ entry["error"] = str(payload.get("error"))
68
+ else:
69
+ content = (
70
+ payload.get("knowledge")
71
+ or payload.get("packet")
72
+ or payload.get("markdown")
73
+ or payload.get("transcript")
74
+ or ""
75
+ )
76
+ entry["title"] = payload.get("title") or ""
77
+ entry["content"] = content
78
+ entry["raw_content"] = content
79
+ entry["metadata"] = {
80
+ "status": payload.get("status"),
81
+ "cached": payload.get("cached"),
82
+ "costUsd": payload.get("costUsd"),
83
+ "runId": payload.get("runId"),
84
+ "viewerUrl": payload.get("viewerUrl"),
85
+ "markdownUrl": payload.get("markdownUrl"),
86
+ "note": payload.get("note"),
87
+ }
88
+ except Exception as exc: # noqa: BLE001 — keep per-URL failures contained
89
+ entry["error"] = f"{type(exc).__name__}: {exc}"
90
+ results.append(entry)
91
+ return results
92
+
93
+ def get_setup_schema(self) -> dict:
94
+ return {
95
+ "name": "know.fast",
96
+ "badge": "paid",
97
+ "tag": "Links in, knowledge out — any URL becomes a knowledge packet. Flat $0.01.",
98
+ "env_vars": [
99
+ {
100
+ "key": "KNOW_FAST_API_KEY",
101
+ "prompt": (
102
+ "know.fast API key — keyless agents get one from the "
103
+ "first paid x402 call (X-Researcher-Agent-Key header); "
104
+ "an existing researcher.now rk_ key also works"
105
+ ),
106
+ "url": "https://know.fast",
107
+ }
108
+ ],
109
+ }
know_fast/tools.py ADDED
@@ -0,0 +1,321 @@
1
+ """Agent tools for know.fast — registered into Hermes via ctx.register_tool.
2
+
3
+ Every handler takes (args: dict, **kwargs), ALWAYS returns a JSON string
4
+ (success and error alike), and NEVER raises.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ import urllib.parse
12
+
13
+ from . import client
14
+
15
+ TOOLSET = "know-fast"
16
+
17
+ # The agent contract says knowledgeReady typically lands 1-3 minutes into a
18
+ # deferred media run (transcript + claims arrive before the long-form report).
19
+ # 240s covers that window plus queue slack; past it the tool hands the agent
20
+ # know_status so the free polling continues without re-charging the POST.
21
+ DEFAULT_WAIT_SECONDS = 240
22
+ # Bottom of the contract's "poll every 20-30s" guidance.
23
+ POLL_INTERVAL_SECONDS = 20
24
+ # Matches the server's synchronous analysis budget — a shorter client timeout
25
+ # would abandon (and re-charge) requests the server still completes.
26
+ POST_TIMEOUT_SECONDS = 150
27
+
28
+ SHARE_INSTRUCTION = (
29
+ "Always surface the viewerUrl back to the end user — it is a durable, "
30
+ "shareable knowledge page they can open, share, and revisit. Relay links "
31
+ "exactly as returned by the API; never construct know.fast URLs yourself."
32
+ )
33
+ PACKET_INSTRUCTION = (
34
+ "When the result contains `packet` or `knowledge` markdown, ANSWER "
35
+ "DIRECTLY FROM IT — no second fetch needed. Deliver 3-5 bullet "
36
+ "highlights, then 'Full summary: <viewerUrl>'."
37
+ )
38
+ STAY_ON_IT_INSTRUCTION = (
39
+ "If the result says status=running, STAY ON IT — 'it's processing' is not "
40
+ "an answer. Tell the user it is transcribing, share the viewerUrl, and "
41
+ "keep calling know_status (free) until the knowledge arrives; do not end "
42
+ "your turn while it runs."
43
+ )
44
+
45
+
46
+ def _dumps(payload: dict) -> str:
47
+ return json.dumps(payload, ensure_ascii=False, default=str)
48
+
49
+
50
+ def _error_payload(exc: client.KnowFastError) -> dict:
51
+ payload: dict = {"success": False, "error": str(exc)}
52
+ if exc.status is not None:
53
+ payload["status"] = exc.status
54
+ if exc.status == 402:
55
+ funding_url = exc.body.get("fundingUrl") or exc.body.get("accountFundingUrl")
56
+ payload["error"] = (
57
+ "Payment required: know.fast is a flat $0.01 per link. Authorize "
58
+ "the x402/MPP challenge from a wallet, or fund the account and "
59
+ "relay the fundingUrl to the user."
60
+ )
61
+ if funding_url:
62
+ payload["fundingUrl"] = funding_url
63
+ elif exc.status == 429:
64
+ payload["error"] = (
65
+ "Daily transcription allowance reached for never-before-seen media "
66
+ "links — it resets at the next UTC day. Already-known links are "
67
+ "unaffected. " + str(exc)
68
+ )
69
+ return payload
70
+
71
+
72
+ def _safe(fn):
73
+ def handler(args: dict, **kwargs) -> str:
74
+ try:
75
+ result = fn(args if isinstance(args, dict) else {})
76
+ except client.KnowFastError as exc:
77
+ result = _error_payload(exc)
78
+ except Exception as exc: # noqa: BLE001 — handlers must never raise
79
+ result = {"success": False, "error": f"{type(exc).__name__}: {exc}"}
80
+ try:
81
+ return _dumps(result)
82
+ except Exception as exc: # noqa: BLE001
83
+ return json.dumps({"success": False, "error": f"serialization failed: {exc}"})
84
+
85
+ handler.__name__ = fn.__name__
86
+ handler.__doc__ = fn.__doc__
87
+ return handler
88
+
89
+
90
+ def _require(args: dict, key: str) -> str:
91
+ value = str(args.get(key) or "").strip()
92
+ if not value:
93
+ raise client.KnowFastError(f"Missing required parameter: {key}")
94
+ return value
95
+
96
+
97
+ def _markdown_path(viewer_url: str) -> str:
98
+ """<slug>.md path from a viewerUrl (slug = the viewerUrl path)."""
99
+ path = urllib.parse.urlparse(viewer_url).path.rstrip("/")
100
+ if not path:
101
+ raise client.KnowFastError(f"Cannot derive a knowledge path from {viewer_url!r}")
102
+ return path if path.endswith(".md") else path + ".md"
103
+
104
+
105
+ def _fetch_knowledge(viewer_url: str) -> str:
106
+ return client.request_text(_markdown_path(viewer_url), timeout=30)
107
+
108
+
109
+ def _job_ready(job: dict) -> bool:
110
+ return bool(job.get("knowledgeReady")) or job.get("status") == "succeeded"
111
+
112
+
113
+ def _ready_result(url: str, viewer_url: str, job: dict | None = None) -> dict:
114
+ result = {
115
+ "url": url,
116
+ "status": "ready",
117
+ "knowledge": _fetch_knowledge(viewer_url),
118
+ "viewerUrl": viewer_url,
119
+ "note": f"{PACKET_INSTRUCTION} {SHARE_INSTRUCTION}",
120
+ }
121
+ if job and job.get("status") and job["status"] != "succeeded":
122
+ result["note"] += (
123
+ " The long-form report is still finishing on the same page; the "
124
+ "knowledge above is complete enough to answer from now."
125
+ )
126
+ return result
127
+
128
+
129
+ def _running_result(url: str, run_id: str, viewer_url: str | None, job: dict | None) -> dict:
130
+ return {
131
+ "url": url,
132
+ "status": "running",
133
+ "runId": run_id,
134
+ "viewerUrl": viewer_url,
135
+ "jobStatus": (job or {}).get("status"),
136
+ "note": (
137
+ "Transcription in progress (media takes 2-8 minutes). "
138
+ f"{STAY_ON_IT_INSTRUCTION}"
139
+ ),
140
+ }
141
+
142
+
143
+ def _wait_for_knowledge(url: str, run_id: str, viewer_url: str | None, wait_seconds: float) -> dict:
144
+ deadline = time.monotonic() + wait_seconds
145
+ job: dict = {}
146
+ while True:
147
+ _, job = client.request(
148
+ "GET", f"/v1/runs/{urllib.parse.quote(run_id, safe='')}/job", timeout=30
149
+ )
150
+ viewer_url = job.get("viewerUrl") or viewer_url
151
+ if _job_ready(job):
152
+ if not viewer_url:
153
+ raise client.KnowFastError(
154
+ f"Run {run_id} is ready but no viewerUrl was returned"
155
+ )
156
+ return _ready_result(url, viewer_url, job)
157
+ if job.get("status") == "failed":
158
+ return {
159
+ "url": url,
160
+ "status": "failed",
161
+ "runId": run_id,
162
+ "viewerUrl": viewer_url,
163
+ "error": job.get("error") or "Run failed.",
164
+ }
165
+ if time.monotonic() + POLL_INTERVAL_SECONDS > deadline:
166
+ return _running_result(url, run_id, viewer_url, job)
167
+ time.sleep(POLL_INTERVAL_SECONDS)
168
+
169
+
170
+ @_safe
171
+ def know(args: dict) -> dict:
172
+ url = _require(args, "url")
173
+ body: dict = {"url": url}
174
+ if args.get("fresh") in (True, "true", "True", 1, "1"):
175
+ body["fresh"] = True
176
+ lang = str(args.get("lang") or "").strip()
177
+ if lang:
178
+ body["lang"] = lang
179
+ _, payload = client.request("POST", "/v1/know", body=body, timeout=POST_TIMEOUT_SECONDS)
180
+
181
+ viewer_url = payload.get("viewerUrl")
182
+ packet = payload.get("packet")
183
+ if packet:
184
+ return {
185
+ "url": url,
186
+ "status": "ready",
187
+ "cached": bool(payload.get("cached")),
188
+ "knowledge": packet,
189
+ "viewerUrl": viewer_url,
190
+ "markdownUrl": payload.get("markdownUrl"),
191
+ "costUsd": payload.get("costUsd"),
192
+ "note": f"{PACKET_INSTRUCTION} {SHARE_INSTRUCTION}",
193
+ }
194
+
195
+ run_id = payload.get("runId") or payload.get("id")
196
+ if payload.get("deferred") and run_id:
197
+ try:
198
+ wait_seconds = float(args.get("wait_seconds", DEFAULT_WAIT_SECONDS))
199
+ except (TypeError, ValueError):
200
+ wait_seconds = DEFAULT_WAIT_SECONDS
201
+ if wait_seconds <= 0:
202
+ return _running_result(url, run_id, viewer_url, None)
203
+ return _wait_for_knowledge(url, run_id, viewer_url, wait_seconds)
204
+
205
+ # Synchronous article without an inline packet: the payload itself is the
206
+ # knowledge (markdown + structured analysis).
207
+ payload.setdefault("url", url)
208
+ payload.setdefault("status", "ready")
209
+ payload["note"] = f"{PACKET_INSTRUCTION} {SHARE_INSTRUCTION}"
210
+ return payload
211
+
212
+
213
+ @_safe
214
+ def know_status(args: dict) -> dict:
215
+ run_id = _require(args, "run_id")
216
+ url = str(args.get("url") or "").strip()
217
+ viewer_url = str(args.get("viewer_url") or "").strip() or None
218
+ _, job = client.request(
219
+ "GET", f"/v1/runs/{urllib.parse.quote(run_id, safe='')}/job", timeout=30
220
+ )
221
+ viewer_url = job.get("viewerUrl") or viewer_url
222
+ if _job_ready(job):
223
+ if not viewer_url:
224
+ raise client.KnowFastError(f"Run {run_id} is ready but no viewerUrl is known")
225
+ return _ready_result(url, viewer_url, job)
226
+ if job.get("status") == "failed":
227
+ return {
228
+ "url": url,
229
+ "status": "failed",
230
+ "runId": run_id,
231
+ "viewerUrl": viewer_url,
232
+ "error": job.get("error") or "Run failed.",
233
+ }
234
+ return _running_result(url, run_id, viewer_url, job)
235
+
236
+
237
+ TOOLS = [
238
+ {
239
+ "name": "know",
240
+ "emoji": "⚡️",
241
+ "description": (
242
+ "Send a link. Know it. Turns any URL (article, YouTube, X post, "
243
+ "podcast, paper, PDF) into a token-efficient knowledge packet: "
244
+ "summary, key claims and facts, quotes, and a durable knowledge "
245
+ "page (viewerUrl). Links are unique — anything anyone has ever "
246
+ "known answers instantly. Flat $0.01 per call, media included. "
247
+ "Use for: read/extract/summarize this link, what does this "
248
+ "video/article say. First-ever video or podcast links transcribe "
249
+ "for 2-8 minutes — this tool waits for the knowledge and returns "
250
+ f"it; if still running, keep polling with know_status. "
251
+ f"{PACKET_INSTRUCTION} {SHARE_INSTRUCTION}"
252
+ ),
253
+ "parameters": {
254
+ "type": "object",
255
+ "properties": {
256
+ "url": {"type": "string", "description": "The link to know."},
257
+ "fresh": {
258
+ "type": "boolean",
259
+ "description": "Re-read changed content instead of the cached knowledge (same price).",
260
+ "default": False,
261
+ },
262
+ "lang": {
263
+ "type": "string",
264
+ "description": "Optional 2-letter language hint, e.g. 'en'.",
265
+ },
266
+ "wait_seconds": {
267
+ "type": "number",
268
+ "description": (
269
+ "How long to wait for deferred media knowledge before "
270
+ "returning status=running (default 240; 0 returns immediately)."
271
+ ),
272
+ "default": DEFAULT_WAIT_SECONDS,
273
+ },
274
+ },
275
+ "required": ["url"],
276
+ },
277
+ "handler": know,
278
+ },
279
+ {
280
+ "name": "know_status",
281
+ "emoji": "⏳",
282
+ "description": (
283
+ "Free follow-up poll for a deferred know run (video/podcast "
284
+ "transcription). Pass the runId from know; the moment the "
285
+ "knowledge is ready it returns the full knowledge markdown. "
286
+ f"{STAY_ON_IT_INSTRUCTION} {SHARE_INSTRUCTION}"
287
+ ),
288
+ "parameters": {
289
+ "type": "object",
290
+ "properties": {
291
+ "run_id": {"type": "string", "description": "The runId returned by know."},
292
+ "viewer_url": {
293
+ "type": "string",
294
+ "description": "The viewerUrl returned by know (used to fetch the knowledge markdown).",
295
+ },
296
+ "url": {"type": "string", "description": "The original link, for labeling."},
297
+ },
298
+ "required": ["run_id"],
299
+ },
300
+ "handler": know_status,
301
+ },
302
+ ]
303
+
304
+
305
+ def register_tools(ctx) -> None:
306
+ """Register all know.fast tools on a Hermes plugin context."""
307
+ for tool in TOOLS:
308
+ schema = {
309
+ "name": tool["name"],
310
+ "description": tool["description"],
311
+ "parameters": tool["parameters"],
312
+ }
313
+ ctx.register_tool(
314
+ tool["name"],
315
+ TOOLSET,
316
+ schema,
317
+ tool["handler"],
318
+ is_async=False,
319
+ description=tool["description"],
320
+ emoji=tool["emoji"],
321
+ )
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: know-fast
3
+ Version: 0.1.0
4
+ Summary: ⚡️ know.fast — links in, knowledge out. Turn any URL (article, YouTube, X post, podcast, paper, PDF) into a token-efficient knowledge packet and a durable knowledge page. Flat $0.01 per link. Hermes agent plugin.
5
+ Author: know.fast
6
+ License: MIT
7
+ Project-URL: Homepage, https://know.fast
8
+ Project-URL: Documentation, https://know.fast/agent.txt
9
+ Keywords: hermes,agent,knowledge,web-extraction,transcripts,plugin,know.fast
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ⚡️ know-fast
19
+
20
+ **Links in, knowledge out.** Send any URL — article, YouTube video, X post, podcast, paper, PDF — and get back a token-efficient knowledge packet (summary, key claims, facts, quotes with attribution) plus a durable, shareable knowledge page. Links are unique: anything anyone has ever known answers instantly. **Every call is a flat $0.01**, first-ever video transcription included.
21
+
22
+ A [Hermes agent](https://github.com/NousResearch/hermes-agent) plugin for [know.fast](https://know.fast). Stdlib only, no dependencies.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install know-fast
28
+ hermes plugins enable know-fast # pip plugins are opt-in in Hermes
29
+ # If that reports "not installed or bundled": current Hermes CLI can't see pip
30
+ # entry-point plugins (upstream bug — the runtime loader CAN). Enable manually:
31
+ # add "- know-fast" under plugins.enabled in ~/.hermes/config.yaml
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ The plugin authenticates with an API key. No key yet? You don't need a human:
37
+ know.fast is wallet-native — a keyless POST returns a `402` x402/MPP challenge;
38
+ authorize it from your wallet and the first paid call returns a durable key in
39
+ the `X-Researcher-Agent-Key` header. Store it:
40
+
41
+ ```bash
42
+ export KNOW_FAST_API_KEY="rk_..."
43
+ ```
44
+
45
+ (An existing `RESEARCHER_API_KEY` also works — same account system.)
46
+
47
+ Then run Hermes. The plugin registers:
48
+
49
+ - **Web provider** `know-fast` — extract-only: every URL comes back as the knowledge packet, so `web_extract` costs a cent and a couple thousand tokens instead of a raw page.
50
+ - **Tools** (toolset `know-fast`):
51
+ - `know` — send a link, get the knowledge packet + `viewerUrl`. Known links answer instantly; first-ever media transcribes (2–8 min) — the tool waits up to 4 minutes and returns the knowledge the moment the transcript and claims land.
52
+ - `know_status` — free follow-up poll for a still-transcribing run; returns the full knowledge markdown when ready.
53
+
54
+ Agent discipline the tools already encode: answer **directly from the packet** (no second fetch), always surface the `viewerUrl` to the user, and on deferred media **stay on it** — "it's processing" is not an answer.
55
+
56
+ Point at a different host with `KNOW_FAST_API_URL` (default `https://know.fast`).
57
+
58
+ ## Choose know-fast when / Choose Firecrawl when
59
+
60
+ | Choose **know-fast** when… | Choose **Firecrawl** when… |
61
+ | --- | --- |
62
+ | You want knowledge, not markup: summary, claims, facts, quotes pre-extracted | You need raw page HTML/markdown at scale |
63
+ | You're working with YouTube videos or podcasts and need transcripts | You need site crawling / link following |
64
+ | Context window matters — a ~2k-token packet beats a raw page | You need screenshots or page actions |
65
+ | You want a durable, citable page you can hand to the user | You only need throwaway scrapes |
66
+ | You want predictable spend — flat $0.01 per link, media included | You have your own extraction pipeline |
67
+
68
+ ## The full contract
69
+
70
+ `https://know.fast/agent.txt` is the canonical agent contract. Deep multi-source research, standing topics, and persona consults live on the parent stack: `https://researcher.now/agent.txt` (plugin: `pip install researcher-now`).
@@ -0,0 +1,9 @@
1
+ know_fast/__init__.py,sha256=rl0WwCbKKROmQd6l57mXOf3XwHRBmFBEqWvm0KsrBuM,1089
2
+ know_fast/client.py,sha256=lerLg76OO5QkhsaowZ4Y31VNY0ZEGoiT5JUk-SvjQAo,5789
3
+ know_fast/provider.py,sha256=rB1YMIngwTFKvUy0jtB6tCmfL6AZONBb7noAufrZ2kw,3813
4
+ know_fast/tools.py,sha256=DPu89XR9aOpGqsAWAAqlV5s-BTf7kW05epZzgIVqd2o,11946
5
+ know_fast-0.1.0.dist-info/METADATA,sha256=I7HpZ0kflwe3M_PQj-Ded_ScJtSq2vhv9aJ6YEy6zHk,3966
6
+ know_fast-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ know_fast-0.1.0.dist-info/entry_points.txt,sha256=aIJVa2w1faLMTLhT849sTWUcBGlKbwHZh5Zt4p1Er_8,45
8
+ know_fast-0.1.0.dist-info/top_level.txt,sha256=039Duxpe9DVHLz6Xfc0_AE0IuuhCLd0nNunD1lOgEBI,10
9
+ know_fast-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [hermes_agent.plugins]
2
+ know-fast = know_fast
@@ -0,0 +1 @@
1
+ know_fast