Initial Arka plugin registry: Official plugins mirror + Arka-signed index

11 plugins from github.com/librefang/librefang-registry plugins/.
index.json / index.json.sig are signed with Arka's Ed25519 key
(not upstream stats.librefang.ai). Private key is not in this repo.
This commit is contained in:
ixoblakp committed 2026-09-01 10:22:07 +03:00
commit 8dec8f6038
62 files changed
+5189

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
# todo-tracker
Detects action items and tasks mentioned in conversations, persists them, and recalls them as context. Helps agents keep track of what needs to be done without the user having to repeat themselves.
## How it works
### Task detection
After each conversation turn, the plugin scans **all** messages (both user and assistant) for task patterns:
- `TODO: ...` or `todo: ...`
- `remind me to ...`
- `don't forget to ...`
- `action item: ...`
- `need to ...`
- `I should ...` or `we should ...`
- `- [ ] ...` (markdown checkbox)
### Completion detection
The plugin also detects when tasks are marked as done:
- `done with ...`
- `completed ...`
- `finished ...`
- Completion markers near task text (checkmark emoji, `[x]`)
### Deduplication
New tasks are deduplicated against existing ones using normalized lowercase comparison and substring matching, so "Fix the login bug" and "fix the login bug" are treated as the same item.
### Limits
Only the latest 20 pending items are kept (FIFO). Completed items are retained for reference.
## Hooks
| Hook | Script | Description |
|------|--------|-------------|
| ingest | `hooks/ingest.py` | Returns pending todo items as a memory fragment |
| after_turn | `hooks/after_turn.py` | Scans messages for tasks and completions, updates the todo list |
## Storage
Todos are stored at `~/.librefang/plugins/todo-tracker/{agent_id}.json` in the format:
```json
{
"todos": [
{"text": "Fix the login bug", "status": "pending", "added": "2026-03-21T12:00:00+00:00"},
{"text": "Update README", "status": "done", "added": "2026-03-21T12:00:00+00:00", "completed": "2026-03-21T13:00:00+00:00"}
]
}
```
## Usage
Installed automatically when enabled in agent configuration.
+300
View File
@@ -0,0 +1,300 @@
#!/usr/bin/env python3
"""Todo tracker after_turn hook.
Scans all conversation messages for task patterns and completion markers,
then persists the updated todo list to disk.
Receives via stdin:
{"type": "after_turn", "agent_id": "...", "messages": [...]}
Prints to stdout:
{"type": "ok"}
"""
import json
import os
import re
import sys
from datetime import datetime, timezone
# Maximum number of pending todos to keep (oldest dropped first)
MAX_PENDING = 20
# Maximum characters to extract for a single task description
MAX_TASK_LEN = 100
# ── Task detection patterns ──────────────────────────────────────────
# Each pattern captures the task text in group 1
TASK_PATTERNS = [
# "TODO: ..." or "todo: ..."
re.compile(r"(?i)\btodo\s*:\s*(.+)"),
# "remind me to ..."
re.compile(r"(?i)\bremind\s+me\s+to\s+(.+)"),
# "don't forget to ..."
re.compile(r"(?i)\bdon'?t\s+forget\s+to\s+(.+)"),
# "action item: ..."
re.compile(r"(?i)\baction\s+item\s*:\s*(.+)"),
# "need to ..."
re.compile(r"(?i)\bneed\s+to\s+(.+)"),
# "I should ..." or "we should ..."
re.compile(r"(?i)\b(?:i|we)\s+should\s+(.+)"),
# "- [ ] ..." markdown checkbox
re.compile(r"-\s*\[\s*\]\s*(.+)"),
]
# ── Completion detection patterns ────────────────────────────────────
COMPLETION_PATTERNS = [
# "done with ..."
re.compile(r"(?i)\bdone\s+with\s+(.+)"),
# "completed ..."
re.compile(r"(?i)\bcompleted\s+(.+)"),
# "finished ..."
re.compile(r"(?i)\bfinished\s+(.+)"),
]
# Completion markers that apply to nearby text
COMPLETION_MARKERS = re.compile(r"(?:\u2705|\[x\])", re.IGNORECASE)
# Pattern to extract text near a completion marker
# Looks for marker followed by text, or text followed by marker
MARKER_CONTEXT = re.compile(
r"(?:\u2705|\[x\])\s*(.+?)(?:\.|$)|(.+?)\s*(?:\u2705|\[x\])",
re.IGNORECASE,
)
def get_storage_dir():
"""Return the plugin storage directory, creating it if needed."""
home = os.path.expanduser("~")
path = os.path.join(home, ".librefang", "plugins", "todo-tracker")
os.makedirs(path, exist_ok=True)
return path
def get_todos_path(agent_id):
"""Return the path to the todos file for the given agent."""
return os.path.join(get_storage_dir(), f"{agent_id}.json")
def load_todos(agent_id):
"""Load existing todos from disk. Returns a list of todo dicts."""
path = get_todos_path(agent_id)
if not os.path.isfile(path):
return []
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("todos", [])
except (OSError, IOError, json.JSONDecodeError):
return []
def save_todos(agent_id, todos):
"""Persist the todo list to disk."""
path = get_todos_path(agent_id)
with open(path, "w", encoding="utf-8") as f:
json.dump({"todos": todos}, f, indent=2, ensure_ascii=False)
def clean_task_text(text):
"""Clean and truncate extracted task text."""
# Take up to end of first sentence or MAX_TASK_LEN
text = text.strip()
# Truncate at sentence boundary
for delim in (".", "!", "\n"):
idx = text.find(delim)
if 0 < idx < MAX_TASK_LEN:
text = text[:idx]
break
text = text.strip().rstrip(".,;:!?")
if len(text) > MAX_TASK_LEN:
text = text[:MAX_TASK_LEN].rstrip()
return text
def normalize_for_comparison(text):
"""Normalize text for fuzzy deduplication: lowercase + strip non-alnum."""
return re.sub(r"[^a-z0-9]", "", text.lower())
def is_duplicate(new_text, existing_todos):
"""Check if a task is a fuzzy duplicate of any existing todo."""
normalized_new = normalize_for_comparison(new_text)
if not normalized_new:
return True # empty tasks are always "duplicates"
for todo in existing_todos:
normalized_existing = normalize_for_comparison(todo.get("text", ""))
if normalized_new == normalized_existing:
return True
# Check if one is a substring of the other (for near-duplicates)
if len(normalized_new) >= 5 and len(normalized_existing) >= 5:
if normalized_new in normalized_existing or normalized_existing in normalized_new:
return True
return False
def get_content(msg):
"""Extract text content from a message object."""
if isinstance(msg, dict):
return msg.get("content", "") or ""
return str(msg)
def extract_tasks_from_text(text):
"""Extract task descriptions from a block of text."""
tasks = []
for pattern in TASK_PATTERNS:
for match in pattern.finditer(text):
raw = match.group(1)
cleaned = clean_task_text(raw)
if cleaned and len(cleaned) >= 3:
tasks.append(cleaned)
return tasks
def extract_completions_from_text(text):
"""Extract completed task descriptions from a block of text."""
completions = []
# Explicit completion phrases
for pattern in COMPLETION_PATTERNS:
for match in pattern.finditer(text):
raw = match.group(1)
cleaned = clean_task_text(raw)
if cleaned and len(cleaned) >= 3:
completions.append(cleaned)
# Completion markers (checkmark emoji, [x])
for match in MARKER_CONTEXT.finditer(text):
raw = match.group(1) or match.group(2) or ""
cleaned = clean_task_text(raw)
if cleaned and len(cleaned) >= 3:
completions.append(cleaned)
return completions
def stem_word(word):
"""Minimal suffix stripping to normalize verb forms (ing, ed, s, etc.).
This is intentionally simple -- just enough to match "fixing" to "fix",
"updated" to "updat(e)", etc. without pulling in nltk.
"""
if len(word) <= 4:
return word
if word.endswith("ing") and len(word) > 5:
# running -> runn -> run (but we keep the stem for comparison)
return word[:-3]
if word.endswith("ed") and len(word) > 4:
return word[:-2]
if word.endswith("s") and not word.endswith("ss") and len(word) > 4:
return word[:-1]
return word
def word_overlap_score(text_a, text_b):
"""Compute a word-overlap similarity score between two texts.
Returns a float between 0.0 and 1.0 indicating what fraction of
the shorter text's stemmed words appear in the longer text.
"""
words_a = set(stem_word(w) for w in re.findall(r"[a-z]+", text_a.lower()) if len(w) >= 3)
words_b = set(stem_word(w) for w in re.findall(r"[a-z]+", text_b.lower()) if len(w) >= 3)
if not words_a or not words_b:
return 0.0
smaller = words_a if len(words_a) <= len(words_b) else words_b
larger = words_b if len(words_a) <= len(words_b) else words_a
overlap = smaller & larger
return len(overlap) / len(smaller)
# Minimum word-overlap score to consider a completion matching a todo
COMPLETION_MATCH_THRESHOLD = 0.6
def mark_completed(todos, completions):
"""Mark todos as done if they match any completion descriptions."""
if not completions:
return todos
now = datetime.now(timezone.utc).isoformat()
completion_norms = [normalize_for_comparison(c) for c in completions]
for todo in todos:
if todo.get("status") == "done":
continue
todo_text = todo.get("text", "")
todo_norm = normalize_for_comparison(todo_text)
for i, comp_norm in enumerate(completion_norms):
if not comp_norm or not todo_norm:
continue
# Exact substring match (normalized)
if comp_norm in todo_norm or todo_norm in comp_norm:
todo["status"] = "done"
todo["completed"] = now
break
# Fuzzy word-overlap match (handles verb form differences)
if word_overlap_score(completions[i], todo_text) >= COMPLETION_MATCH_THRESHOLD:
todo["status"] = "done"
todo["completed"] = now
break
return todos
def enforce_pending_limit(todos):
"""Keep only the latest MAX_PENDING pending items. Done items are kept."""
pending = [t for t in todos if t.get("status") == "pending"]
done = [t for t in todos if t.get("status") != "pending"]
if len(pending) > MAX_PENDING:
# Keep the most recent MAX_PENDING pending items (by position / added time)
pending = pending[-MAX_PENDING:]
return pending + done
def main():
request = json.loads(sys.stdin.read())
agent_id = request.get("agent_id", "unknown")
messages = request.get("messages", [])
todos = load_todos(agent_id)
now = datetime.now(timezone.utc).isoformat()
all_new_tasks = []
all_completions = []
# Scan ALL messages for task and completion patterns
for msg in messages:
content = get_content(msg)
if not content.strip():
continue
all_new_tasks.extend(extract_tasks_from_text(content))
all_completions.extend(extract_completions_from_text(content))
# Add new tasks (deduplicated against existing)
for task_text in all_new_tasks:
if not is_duplicate(task_text, todos):
todos.append({
"text": task_text,
"status": "pending",
"added": now,
})
# Mark completed tasks
todos = mark_completed(todos, all_completions)
# Enforce pending limit
todos = enforce_pending_limit(todos)
# Persist
save_todos(agent_id, todos)
print(json.dumps({"type": "ok"}), flush=True)
if __name__ == "__main__":
main()
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Todo tracker ingest hook.
Returns pending todo items as a memory fragment so agents stay aware
of outstanding action items during conversations.
Receives via stdin:
{"type": "ingest", "agent_id": "...", "message": "user message text"}
Prints to stdout:
{"type": "ingest_result", "memories": [{"content": "..."}]}
"""
import json
import os
import sys
def get_todos_path(agent_id):
"""Return the path to the todos file for the given agent."""
home = os.path.expanduser("~")
return os.path.join(
home, ".librefang", "plugins", "todo-tracker", f"{agent_id}.json"
)
def load_todos(agent_id):
"""Load existing todos from disk. Returns a list of todo dicts."""
path = get_todos_path(agent_id)
if not os.path.isfile(path):
return []
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("todos", [])
except (OSError, IOError, json.JSONDecodeError):
return []
def main():
request = json.loads(sys.stdin.read())
agent_id = request.get("agent_id", "unknown")
memories = []
todos = load_todos(agent_id)
# Filter to pending items only
pending = [t for t in todos if t.get("status") == "pending"]
if pending:
items = []
for i, todo in enumerate(pending, 1):
items.append(f"{i}) {todo.get('text', '?')}")
items_str = " ".join(items)
memories.append(
{"content": f"[todo] Pending items: {items_str}"}
)
print(json.dumps({"type": "ingest_result", "memories": memories}))
if __name__ == "__main__":
main()
+41
View File
@@ -0,0 +1,41 @@
name = "todo-tracker"
version = "0.1.0"
description = "Detects action items and tasks mentioned in conversations, persists them, and recalls them as context"
author = "librefang"
[hooks]
ingest = "hooks/ingest.py"
after_turn = "hooks/after_turn.py"
[i18n.zh]
name = "待办追踪"
description = "识别对话中的行动项与任务,持久化保存并作为上下文召回。"
[i18n.zh-TW]
name = "待辦追蹤"
description = "辨識對話中的行動項目與任務,持久化保存並作為上下文召回。"
[i18n.ja]
name = "ToDo トラッカー"
description = "会話内のアクションアイテム・タスクを検出し、永続化して文脈として呼び戻す。"
[i18n.ko]
name = "할 일 추적기"
description = "대화에서 액션 아이템과 작업을 감지해 저장하고 컨텍스트로 회상."
[i18n.de]
name = "Todo-Tracker"
description = "Erkennt in Konversationen genannte Action-Items und Aufgaben, speichert sie und ruft sie als Kontext zurück."
[i18n.es]
name = "Rastreador de tareas"
description = "Detecta tareas y acciones mencionadas en las conversaciones, las persiste y las recupera como contexto."
[i18n.fr]
name = "Suivi de tâches"
description = "Détecte les actions et tâches mentionnées dans les conversations, les persiste et les rappelle comme contexte."
[integrity]
"hooks/after_turn.py" = "5c9b524e0c2880152ddad2fcebb6cbe6fc548e3030871d2805a9a789eed80fdc"
"hooks/ingest.py" = "1cd6a11c07e94dd34bb335f21c19c353b74016ba67a0ebd9bd4cd7cbf227705f"
+1
View File
@@ -0,0 +1 @@
# No external dependencies — uses only Python stdlib