From 5fb0be98bd2ea32d96bd9592478c7675eeb7ccbf Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Sun, 19 Jul 2026 09:27:18 +0300 Subject: [PATCH] docs: add safe Hermes Tweet context --- README.md | 14 ++++++++++ SKILL.md | 8 ++++++ skills/awaek/SKILL.md | 8 ++++++ skills/awaek/scripts/db.py | 51 ++++++++++++++++++++++++----------- tests/test_db_self_test.py | 54 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 tests/test_db_self_test.py diff --git a/README.md b/README.md index 77f8601..5761d2c 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,20 @@ xurl auth status xurl whoami ``` +## Optional Hermes Tweet Context + +Hermes users can install Hermes Tweet as a companion plugin: +https://github.com/Xquik-dev/hermes-tweet + +Keep `xurl` as Awaek's source of truth for private bookmark sync. Use Hermes +Tweet only for read-only public context around saved posts, such as public tweet +reads, replies, user lookup, trends, and keyword monitoring. This is useful when +a bookmark needs current public discussion or profile context before drafting. + +Hermes Tweet read tools use `XQUIK_API_KEY`. Leave +`HERMES_TWEET_ENABLE_ACTIONS` unset for Awaek workflows so write actions remain +unavailable. + ## Sync Bookmarks In Hermes or OpenClaw, ask: diff --git a/SKILL.md b/SKILL.md index c40b8c9..fbad5fa 100644 --- a/SKILL.md +++ b/SKILL.md @@ -19,6 +19,8 @@ Do not answer Awaek requests from OpenClaw memory alone. Retrieve local bookmark - Data stays local. New installs use `~/.awaek/data/awaek.db`; existing Hermes installs may keep using `~/.hermes/awaek/data/awaek.db`. - X access is handled by the local `xurl` CLI. +- If Hermes Tweet is installed, use it only for read-only public context around saved posts. Do not use it to replace `xurl` bookmark sync. +- Keep `HERMES_TWEET_ENABLE_ACTIONS` unset during Awaek workflows. - Never read, print, summarize, upload, or inspect `~/.xurl`. - Never ask the user to paste X Client IDs, Client Secrets, access tokens, refresh tokens, or `~/.xurl` contents into chat. - Do not run `xurl` with verbose/debug flags. @@ -62,6 +64,12 @@ python3 {baseDir}/skills/awaek/scripts/answer_pack.py --plan-stdin --limit 30 << JSON ``` +Optional public context: + +- Use Hermes Tweet after Awaek evidence retrieval when a saved post needs public reply context, author lookup, current trends, or keyword monitoring. +- Hermes Tweet read tools require `XQUIK_API_KEY`. +- Do not enable Hermes Tweet action tools for bookmark analysis, drafting, planning, or search. + ## Setup And Sync When the user asks to install, set up, or sync Awaek: diff --git a/skills/awaek/SKILL.md b/skills/awaek/SKILL.md index 09803c3..69abd9a 100644 --- a/skills/awaek/SKILL.md +++ b/skills/awaek/SKILL.md @@ -29,6 +29,8 @@ For normal Awaek questions, retrieve local bookmark evidence first, then answer - Awaek is local-first. New installs store bookmark data in `~/.awaek/data/awaek.db`. - Existing Hermes installs may keep using `~/.hermes/awaek/data/awaek.db`; the scripts detect that automatically. - X access is handled by the local `xurl` CLI. +- If Hermes Tweet is installed, use it only for read-only public context around saved posts. Do not use it to replace `xurl` bookmark sync. +- Keep `HERMES_TWEET_ENABLE_ACTIONS` unset during Awaek workflows. - Never read, print, summarize, upload, or inspect `~/.xurl`. - Never ask the user to paste X Client IDs, Client Secrets, access tokens, refresh tokens, or `~/.xurl` contents into chat. - Do not run `xurl` with verbose/debug flags. @@ -72,6 +74,12 @@ python3 ${HERMES_SKILL_DIR}/scripts/answer_pack.py --plan-stdin --limit 30 <<'JS JSON ``` +Optional public context: + +- Use Hermes Tweet after Awaek evidence retrieval when a saved post needs public reply context, author lookup, current trends, or keyword monitoring. +- Hermes Tweet read tools require `XQUIK_API_KEY`. +- Do not enable Hermes Tweet action tools for bookmark analysis, drafting, planning, or search. + ## Setup And Sync If the user asks to install, set up, or sync Awaek, use this flow. diff --git a/skills/awaek/scripts/db.py b/skills/awaek/scripts/db.py index 1cac638..bdae86a 100755 --- a/skills/awaek/scripts/db.py +++ b/skills/awaek/scripts/db.py @@ -3,6 +3,7 @@ import json import re import sqlite3 +import tempfile from datetime import datetime, timezone from pathlib import Path from urllib.parse import urlparse @@ -1117,6 +1118,40 @@ def make_fts_query(query): return " OR ".join(tokens[:12]) +def run_self_test(): + global DATA_DIR, DB_PATH + + configured_data_dir = DATA_DIR + configured_db_path = DB_PATH + try: + with tempfile.TemporaryDirectory(prefix="awaek-self-test-") as temp_dir: + DATA_DIR = Path(temp_dir) + DB_PATH = DATA_DIR / "awaek.db" + init_db() + sample = [ + { + "id": "sample-1", + "tweet_id": "sample-1", + "url": "https://x.com/example/status/sample-1", + "author_username": "example", + "author_name": "Example", + "text": "Marketing launch growth agent payments test bookmark.", + "raw_json": {"sample": True}, + } + ] + result = upsert_bookmarks(sample) + rows = search("marketing", 5) + return { + "ok": bool(rows), + "isolated": True, + "upsert": result, + "results": rows, + } + finally: + DATA_DIR = configured_data_dir + DB_PATH = configured_db_path + + def main(): parser = argparse.ArgumentParser() parser.add_argument("--init", action="store_true") @@ -1134,21 +1169,7 @@ def main(): return if args.self_test: - init_db() - sample = [ - { - "id": "sample-1", - "tweet_id": "sample-1", - "url": "https://x.com/example/status/sample-1", - "author_username": "example", - "author_name": "Example", - "text": "Marketing launch growth agent payments test bookmark.", - "raw": {"sample": True}, - } - ] - result = upsert_bookmarks(sample) - rows = search("marketing", 5) - print(json.dumps({"ok": bool(rows), "upsert": result, "results": rows}, indent=2)) + print(json.dumps(run_self_test(), indent=2)) return parser.print_help() diff --git a/tests/test_db_self_test.py b/tests/test_db_self_test.py new file mode 100644 index 0000000..578709e --- /dev/null +++ b/tests/test_db_self_test.py @@ -0,0 +1,54 @@ +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +SCRIPT_DIR = Path(__file__).resolve().parents[1] / "skills" / "awaek" / "scripts" +sys.path.insert(0, str(SCRIPT_DIR)) + +import db + + +class SelfTestIsolationTests(unittest.TestCase): + def setUp(self): + self.original_data_dir = db.DATA_DIR + self.original_db_path = db.DB_PATH + + def tearDown(self): + db.DATA_DIR = self.original_data_dir + db.DB_PATH = self.original_db_path + + def configure_unwritten_database(self, root): + db.DATA_DIR = root / "configured-data" + db.DB_PATH = db.DATA_DIR / "awaek.db" + + def test_self_test_does_not_write_the_configured_database(self): + with tempfile.TemporaryDirectory() as temp_dir: + self.configure_unwritten_database(Path(temp_dir)) + + result = db.run_self_test() + + self.assertTrue(result["ok"]) + self.assertTrue(result["isolated"]) + self.assertEqual(result["upsert"]["inserted"], 1) + self.assertFalse(db.DB_PATH.exists()) + + def test_self_test_restores_paths_after_failure(self): + with tempfile.TemporaryDirectory() as temp_dir: + self.configure_unwritten_database(Path(temp_dir)) + configured_data_dir = db.DATA_DIR + configured_db_path = db.DB_PATH + + with mock.patch.object(db, "init_db", side_effect=RuntimeError("failed")): + with self.assertRaisesRegex(RuntimeError, "failed"): + db.run_self_test() + + self.assertEqual(db.DATA_DIR, configured_data_dir) + self.assertEqual(db.DB_PATH, configured_db_path) + self.assertFalse(db.DB_PATH.exists()) + + +if __name__ == "__main__": + unittest.main()