Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions skills/awaek/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
51 changes: 36 additions & 15 deletions skills/awaek/scripts/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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()
Expand Down
54 changes: 54 additions & 0 deletions tests/test_db_self_test.py
Original file line number Diff line number Diff line change
@@ -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()