diff --git a/.daemon/logs/daemon.out.log b/.daemon/logs/daemon.out.log index 700831b1d..8fc2a42a5 100644 --- a/.daemon/logs/daemon.out.log +++ b/.daemon/logs/daemon.out.log @@ -130,3 +130,12 @@ In the meantime, you could try bun:sqlite which has a similar API. 02:58:37 INFO  [watcher] File watcher started 02:58:37 INFO  [daemon] Server listening {"address":"::1","port":3850} 02:58:37 INFO  [daemon] Daemon ready +07:51:14 INFO  [daemon] Serving dashboard {"path":"/home/nicholai/signet/signetai/packages/cli/dashboard/build"} +07:51:14 INFO  [daemon] Signet Daemon starting +07:51:14 INFO  [daemon] Agents directory {"path":"/home/nicholai/.agents"} +07:51:14 INFO  [daemon] Port configured {"port":3850} +07:51:14 INFO  [daemon] Memory schema initialized +07:51:14 INFO  [daemon] Process ID {"pid":1076069} +07:51:14 INFO  [watcher] File watcher started +07:51:14 INFO  [daemon] Server listening {"address":"::1","port":3850} +07:51:14 INFO  [daemon] Daemon ready diff --git a/.daemon/pid b/.daemon/pid index f0358340a..113fbc4f5 100644 --- a/.daemon/pid +++ b/.daemon/pid @@ -1 +1 @@ -625468 \ No newline at end of file +1076069 \ No newline at end of file diff --git a/hooks/agent-memory/HOOK.md b/hooks/agent-memory/HOOK.md index 56f1e2d0a..8779c0335 100644 --- a/hooks/agent-memory/HOOK.md +++ b/hooks/agent-memory/HOOK.md @@ -1,119 +1,10 @@ --- name: agent-memory -description: "Signet memory integration - hybrid search, auto-embedding, cross-harness persistence" -homepage: https://signetai.sh -metadata: - { - "openclaw": { - "emoji": "🧠", - "events": ["agent:bootstrap", "command:new", "command:remember", "command:recall", "command:context"], - "install": [{ "id": "workspace", "kind": "workspace", "label": "Workspace hook" }] - } - } +description: "Signet memory integration" --- # Agent Memory Hook (Signet) -Unified memory system for all AI harnesses - OpenClaw, Claude Code, OpenCode, Codex. -Uses the Signet daemon API for all operations (no Python dependency). - -## Requirements - -- Signet daemon running: `signet start` -- Default daemon URL: `http://localhost:3850` -- Override via: `SIGNET_DAEMON_URL` environment variable - -## Features - -- **Hybrid Search**: 70% vector similarity + 30% BM25 keyword matching -- **Auto-Embedding**: Memories are vectorized on save via Ollama/OpenAI -- **Cross-Harness**: Same memory pool for all agents -- **No Python**: Uses native TypeScript daemon - -## Commands - -### `/context` -Load memory context (identity + recent memories) into the session. -Use at the start of a conversation to get full context. - -### `/remember ` -Save a memory with auto-embedding. - -Prefixes: -- `critical:` - pinned memory (never decays) -- `[tag1,tag2]:` - tagged memory - -Examples: -``` -/remember nicholai prefers tabs over spaces -/remember critical: never push directly to main -/remember [voice,tts]: qwen model needs 12GB VRAM minimum -``` - -### `/recall ` -Search memories using hybrid search (semantic + keyword). - -Examples: -``` -/recall voice configuration -/recall signet architecture -/recall preferences -``` - -### On `/new` -Automatically extracts session context before reset. - -## Architecture - -``` -~/.agents/ -├── agent.yaml # Signet configuration -├── MEMORY.md # Synthesized summary -└── memory/ - └── memories.db # SQLite (memories + vectors) -``` - -Daemon: `http://localhost:3850` - -## Embedding Providers - -Configure in `~/.agents/agent.yaml`: - -**Ollama (local, default):** -```yaml -embedding: - provider: ollama - model: nomic-embed-text - dimensions: 768 - base_url: http://localhost:11434 -``` - -**OpenAI-compatible:** -```yaml -embedding: - provider: openai - model: text-embedding-3-small - dimensions: 1536 - base_url: https://api.openai.com/v1 -``` - -## Search Tuning - -Adjust in `~/.agents/agent.yaml`: - -```yaml -search: - alpha: 0.7 # Vector weight (0-1) - top_k: 20 # Candidates per source - min_score: 0.3 # Minimum result score -``` - -## Harness Compatibility - -Works with: -- **OpenClaw** - via this hook -- **Claude Code** - reads ~/.agents/AGENTS.md, MEMORY.md -- **OpenCode** - reads ~/.agents/AGENTS.md -- **Codex** - reads ~/.agents/AGENTS.md - -All harnesses share the same memory database. +- `/context` - Load memory context +- `/remember ` - Save a memory +- `/recall ` - Search memories diff --git a/hooks/agent-memory/handler.js b/hooks/agent-memory/handler.js index 178b601da..ebdcf57c3 100644 --- a/hooks/agent-memory/handler.js +++ b/hooks/agent-memory/handler.js @@ -1,420 +1,48 @@ -/** - * Signet Agent Memory Hook - * - * Unified memory system for all AI harnesses (OpenClaw, Claude Code, OpenCode, Codex) - * Uses the Signet daemon API for memory operations - no Python dependency. - * - * Features: - * - Hybrid search (vector + BM25) - * - Auto-embedding via daemon - * - Cross-harness persistence - * - * Spec: Signet v0.2.1 - https://signetai.sh - */ -import path from "node:path"; +import { spawn } from "node:child_process"; import os from "node:os"; -import fs from "node:fs/promises"; -const DAEMON_URL = process.env.SIGNET_DAEMON_URL || "http://localhost:3850"; -const MEMORY_MD_PATH = path.join(os.homedir(), ".agents/MEMORY.md"); -const AGENTS_DIR = path.join(os.homedir(), ".agents"); +const MEMORY_SCRIPT = "/home/nicholai/.agents/memory/scripts/memory.py"; -/** - * Check if the Signet daemon is running - */ -async function isDaemonRunning() { - try { - const res = await fetch(`${DAEMON_URL}/health`, { - signal: AbortSignal.timeout(1000), - }); - return res.ok; - } catch { - return false; - } -} - -/** - * Call the Signet daemon remember API - */ -async function daemonRemember(content, options = {}) { - const res = await fetch(`${DAEMON_URL}/api/memory/remember`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - content, - type: options.type, - importance: options.importance, - tags: options.tags, - who: options.who || "openclaw", - }), - signal: AbortSignal.timeout(10000), +async function runMemoryScript(args) { + return new Promise((resolve, reject) => { + const proc = spawn("python3", [MEMORY_SCRIPT, ...args], { timeout: 5000 }); + let stdout = "", stderr = ""; + proc.stdout.on("data", (d) => { stdout += d.toString(); }); + proc.stderr.on("data", (d) => { stderr += d.toString(); }); + proc.on("close", (code) => { + if (code === 0) resolve(stdout.trim()); + else reject(new Error(stderr || `exit code ${code}`)); }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`Daemon remember failed: ${res.status} - ${text}`); - } - - return res.json(); + proc.on("error", reject); + }); } -/** - * Call the Signet daemon recall API - */ -async function daemonRecall(query, options = {}) { - const res = await fetch(`${DAEMON_URL}/api/memory/recall`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query, - limit: options.limit || 10, - type: options.type, - min_score: options.minScore, - }), - signal: AbortSignal.timeout(10000), - }); +const handler = async (event) => { + if (event.type !== "command") return; + const args = event.context?.args || ""; - if (!res.ok) { - const text = await res.text(); - throw new Error(`Daemon recall failed: ${res.status} - ${text}`); - } - - return res.json(); -} - -/** - * Call the session-start hook to get context - */ -async function daemonSessionStart(harness, options = {}) { - const res = await fetch(`${DAEMON_URL}/api/hooks/session-start`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - harness, - sessionKey: options.sessionKey, - context: options.context, - }), - signal: AbortSignal.timeout(5000), - }); - - if (!res.ok) { - return null; - } - - return res.json(); -} - -/** - * Detect harness name from session key - */ -function detectHarness(sessionKey) { - if (!sessionKey) return "openclaw"; - if (sessionKey.includes("claude")) return "claude-code"; - if (sessionKey.includes("opencode")) return "opencode"; - return "openclaw"; -} - -/** - * Read recent messages from session file for memory extraction - */ -async function getRecentSessionContent(sessionFilePath) { - try { - const content = await fs.readFile(sessionFilePath, "utf-8"); - const lines = content.trim().split("\n"); - const recentLines = lines.slice(-20); - - const messages = []; - for (const line of recentLines) { - try { - const entry = JSON.parse(line); - if (entry.type === "message" && entry.message) { - const msg = entry.message; - const role = msg.role; - if ((role === "user" || role === "assistant") && msg.content) { - const text = Array.isArray(msg.content) - ? msg.content.find((c) => c.type === "text")?.text - : msg.content; - if (text && !text.startsWith("/")) { - messages.push(`${role}: ${text}`); - } - } - } - } catch { - // Skip invalid JSON lines - } - } - return messages.join("\n"); - } catch { - return null; - } -} - -/** - * Parse remember content for prefixes - * - `critical:` for pinned memories - * - `[tag1,tag2]:` for tagged memories - */ -function parseRememberContent(content) { - let type = "explicit"; - let importance = 0.8; - let tags = []; - let cleanContent = content.trim(); - - // Check for critical prefix - if (cleanContent.toLowerCase().startsWith("critical:")) { - type = "critical"; - importance = 1.0; - cleanContent = cleanContent.slice(9).trim(); - } - // Check for tag prefix [tag1,tag2]: - else if (cleanContent.startsWith("[") && cleanContent.includes("]:")) { - const tagMatch = cleanContent.match(/^\[([^\]]+)\]:\s*/); - if (tagMatch) { - tags = tagMatch[1].split(",").map((t) => t.trim()); - cleanContent = cleanContent.slice(tagMatch[0].length).trim(); - } - } - - return { content: cleanContent, type, importance, tags }; -} - -/** - * Handle /remember command - */ -async function handleRemember(event) { - const context = event.context || {}; - const args = context.args || ""; - - if (!args.trim()) { - event.messages.push( - "🧠 Usage: /remember \n\n" + - "Prefixes:\n" + - "- `critical:` for pinned memories\n" + - "- `[tag1,tag2]:` for tagged memories" - ); - return; - } - - const harness = detectHarness(event.sessionKey); - const parsed = parseRememberContent(args); - - try { - const result = await daemonRemember(parsed.content, { - type: parsed.type, - importance: parsed.importance, - tags: parsed.tags, - who: harness, - }); - - event.messages.push(`🧠 Memory saved (id: ${result.id || result.memoryId})`); - } catch (err) { - event.messages.push(`🧠 Error saving memory: ${err.message}`); - } -} - -/** - * Handle /recall command - */ -async function handleRecall(event) { - const context = event.context || {}; - const args = context.args || ""; - - if (!args.trim()) { - event.messages.push("🧠 Usage: /recall "); - return; - } - - try { - const result = await daemonRecall(args.trim(), { limit: 10 }); - const memories = result.results || []; - - if (memories.length === 0) { - event.messages.push("🧠 No memories found matching your query."); - return; - } - - const formatted = memories - .map((m, i) => { - const date = new Date(m.created_at).toLocaleDateString(); - const score = (m.score * 100).toFixed(0); - return `${i + 1}. [${date}] (${score}%) ${m.content}`; - }) - .join("\n"); - - event.messages.push(`🧠 Memory search results:\n\n${formatted}`); - } catch (err) { - event.messages.push(`🧠 Error querying memory: ${err.message}`); - } -} - -/** - * Handle /context command - load memory context for current session - */ -async function handleContext(event) { - const harness = detectHarness(event.sessionKey); - - try { - const result = await daemonSessionStart(harness, { - sessionKey: event.sessionKey, - }); - - if (result && result.inject) { - event.messages.push(`🧠 **Memory Context Loaded**\n\n${result.inject}`); - } else { - // Fallback to reading MEMORY.md directly - try { - const memoryMd = await fs.readFile(MEMORY_MD_PATH, "utf-8"); - if (memoryMd.trim()) { - event.messages.push(`🧠 **Memory Context**\n\n${memoryMd.trim()}`); - } else { - event.messages.push("🧠 No memory context available."); - } - } catch { - event.messages.push("🧠 No memory context available."); - } - } - } catch (err) { - // Fallback to MEMORY.md - try { - const memoryMd = await fs.readFile(MEMORY_MD_PATH, "utf-8"); - if (memoryMd.trim()) { - event.messages.push(`🧠 **Memory Context**\n\n${memoryMd.trim()}`); - } else { - event.messages.push("🧠 No memory context available."); - } - } catch { - event.messages.push(`🧠 Error loading context: ${err.message}`); - } - } -} - -/** - * Handle /new command - save session context before reset - */ -async function handleNew(event) { - const context = event.context || {}; - const sessionEntry = context.previousSessionEntry || context.sessionEntry || {}; - const sessionFile = sessionEntry.sessionFile; - - if (!sessionFile) { - console.log("[agent-memory] No session file found, skipping auto-save"); - return; - } - - try { - const sessionContent = await getRecentSessionContent(sessionFile); - if (!sessionContent || sessionContent.length < 100) { - console.log("[agent-memory] Session too short for auto-extraction"); - return; - } - - const harness = detectHarness(event.sessionKey); - const now = new Date().toISOString(); - const summaryContent = `[session-end]: Session ended at ${now}. Recent conversation:\n${sessionContent.slice(0, 500)}`; - - await daemonRemember(summaryContent, { - type: "session", - importance: 0.6, - who: harness, - }); - - console.log("[agent-memory] Session context saved to Signet"); - } catch (err) { - console.error("[agent-memory] Failed to save session memory:", err.message); - } -} - -/** - * Handle agent:bootstrap event - inject memories into bootstrap context - */ -async function handleBootstrap(event) { - const harness = detectHarness(event.sessionKey); - - try { - const running = await isDaemonRunning(); - if (!running) { - console.log("[agent-memory] Signet daemon not running, skipping memory injection"); - return; - } - - const result = await daemonSessionStart(harness, { - sessionKey: event.sessionKey, - }); - - if (!result || !result.memories || result.memories.length === 0) { - console.log("[agent-memory] No memories to inject"); - return; - } - - // Add a SIGNET.md file to bootstrap context with recent memories - const context = event.context || {}; - const bootstrapFiles = context.bootstrapFiles || []; - - // Build memory content - const memoryContent = [ - "# Recent Memories (Signet)", - "", - ...result.memories.slice(0, 10).map((m) => { - const date = new Date(m.created_at).toLocaleDateString(); - return `- [${date}] ${m.content}`; - }), - ].join("\n"); - - // Add SIGNET.md to bootstrap files - bootstrapFiles.push({ - name: "SIGNET.md", - content: memoryContent, - required: false, - }); - - context.bootstrapFiles = bootstrapFiles; - - console.log(`[agent-memory] Injected ${result.memories.length} memories into bootstrap context`); - } catch (err) { - console.warn("[agent-memory] Failed to inject memories:", err.message); - } -} - -/** - * Main hook handler - */ -const agentMemoryHandler = async (event) => { - // Handle agent:bootstrap event for memory injection - if (event.type === "agent" && event.action === "bootstrap") { - await handleBootstrap(event); - return; - } - - // Check if daemon is running for memory commands - if (event.type === "command" && ["remember", "recall", "context"].includes(event.action)) { - const running = await isDaemonRunning(); - if (!running) { - console.warn("[agent-memory] Signet daemon not running at", DAEMON_URL); - event.messages.push( - "🧠 Signet daemon not running. Start it with: `signet start`" - ); - return; - } - } - - if (event.type !== "command") { - return; - } - - switch (event.action) { - case "remember": - await handleRemember(event); - break; - case "recall": - await handleRecall(event); - break; - case "context": - await handleContext(event); - break; - case "new": - await handleNew(event); - break; - } + switch (event.action) { + case "remember": + if (!args.trim()) { event.messages.push("🧠 Usage: /remember "); return; } + try { + const result = await runMemoryScript(["save", "--mode", "explicit", "--who", "openclaw", "--content", args.trim()]); + event.messages.push(`🧠 ${result}`); + } catch (e) { event.messages.push(`🧠 Error: ${e.message}`); } + break; + case "recall": + if (!args.trim()) { event.messages.push("🧠 Usage: /recall "); return; } + try { + const result = await runMemoryScript(["query", args.trim(), "--limit", "10"]); + event.messages.push(result ? `🧠 Results:\n\n${result}` : "🧠 No memories found."); + } catch (e) { event.messages.push(`🧠 Error: ${e.message}`); } + break; + case "context": + try { + const result = await runMemoryScript(["load", "--mode", "session-start"]); + event.messages.push(result ? `🧠 **Context**\n\n${result}` : "🧠 No context."); + } catch (e) { event.messages.push(`🧠 Error: ${e.message}`); } + break; + } }; -export default agentMemoryHandler; +export default handler; diff --git a/hooks/agent-memory/package.json b/hooks/agent-memory/package.json index 3dbc1ca59..7f8da06e7 100644 --- a/hooks/agent-memory/package.json +++ b/hooks/agent-memory/package.json @@ -1,3 +1,5 @@ { + "name": "agent-memory", + "version": "1.0.0", "type": "module" -} +} \ No newline at end of file diff --git a/memory/memories.db b/memory/memories.db index f0aa97f65..c645133f0 100644 Binary files a/memory/memories.db and b/memory/memories.db differ diff --git a/skills/registry.json b/skills/registry.json new file mode 100644 index 000000000..70b7de2ba --- /dev/null +++ b/skills/registry.json @@ -0,0 +1,9 @@ +{ + "skills": {}, + "sources": [ + { + "type": "claude-code", + "path": "/home/nicholai/.claude/skills" + } + ] +} \ No newline at end of file