2026-02-18_signet-setup

This commit is contained in:
Nicholai Vogel 2026-02-18 00:51:14 -07:00
parent acfddae0ff
commit 99b54e164b
7 changed files with 65 additions and 526 deletions

View File

@ -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

View File

@ -1 +1 @@
625468
1076069

View File

@ -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 <content>`
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 <query>`
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 <content>` - Save a memory
- `/recall <query>` - Search memories

View File

@ -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 <content>\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 <search query>");
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 <content>"); 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 <query>"); 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;

View File

@ -1,3 +1,5 @@
{
"name": "agent-memory",
"version": "1.0.0",
"type": "module"
}
}

Binary file not shown.

9
skills/registry.json Normal file
View File

@ -0,0 +1,9 @@
{
"skills": {},
"sources": [
{
"type": "claude-code",
"path": "/home/nicholai/.claude/skills"
}
]
}