sync: skills and tool contract overhaul from private repo

This commit is contained in:
“BeeRad”
2026-03-07 11:45:26 +11:00
parent 46ac8d9bde
commit 2cdd685c04
32 changed files with 2548 additions and 681 deletions
+23 -23
View File
@@ -41,39 +41,39 @@ Restart Claude. Done.
## What to Expect
Once connected, Claude will:
- **Call `rah_get_context` first** to orient itself (stats, hub nodes, dimensions, guides)
- **Call `getContext` first** to orient itself (stats, hub nodes, dimensions, skills)
- **Proactively capture knowledge** — when a new insight, decision, person, or reference surfaces, it proposes a specific node (title, dimensions, description) so you can approve with minimal friction
- **Read guides for complex tasks** — system guides (immutable) teach it how your graph works; custom guides teach it your workflows
- **Read skills for complex tasks** — skills are editable and shared across internal + external agents
- **Search before creating** to avoid duplicates
## Available Tools
| Tool | Description |
|------|-------------|
| `rah_get_context` | Get graph overview — stats, hub nodes, dimensions, recent activity |
| `rah_add_node` | Create a new node |
| `rah_search_nodes` | Search nodes by keyword |
| `rah_get_nodes` | Load nodes by ID (includes chunk + metadata) |
| `rah_update_node` | Update an existing node |
| `rah_create_edge` | Create connection between nodes |
| `rah_update_edge` | Update an edge explanation |
| `rah_query_edges` | Find edges for a node |
| `rah_list_dimensions` | List all dimensions |
| `rah_create_dimension` | Create a dimension |
| `rah_update_dimension` | Update/rename a dimension |
| `rah_delete_dimension` | Delete a dimension |
| `rah_list_guides` | List available guides (system + custom) |
| `rah_read_guide` | Read a guide by name |
| `rah_write_guide` | Create or update a custom guide |
| `rah_delete_guide` | Delete a custom guide |
| `rah_search_content` | Search through source content (transcripts, books, articles) |
| `rah_sqlite_query` | Execute read-only SQL queries (SELECT/WITH/PRAGMA) |
| `getContext` | Get graph overview — stats, hub nodes, dimensions, recent activity |
| `createNode` | Create a new node |
| `queryNodes` | Search nodes by keyword |
| `getNodesById` | Load nodes by ID (includes chunk + metadata) |
| `updateNode` | Update an existing node |
| `createEdge` | Create connection between nodes |
| `updateEdge` | Update an edge explanation |
| `queryEdge` | Find edges for a node |
| `queryDimensions` | List all dimensions |
| `createDimension` | Create a dimension |
| `updateDimension` | Update/rename a dimension |
| `deleteDimension` | Delete a dimension |
| `listSkills` | List available skills |
| `readSkill` | Read a skill by name |
| `writeSkill` | Create or update a skill |
| `deleteSkill` | Delete a skill |
| `searchContentEmbeddings` | Search through source content (transcripts, books, articles) |
| `sqliteQuery` | Execute read-only SQL queries (SELECT/WITH/PRAGMA) |
## Guides
## Skills
Guides are detailed instruction sets that teach Claude how to work with your knowledge base. System guides (schema, creating-nodes, edges, dimensions, extract) are bundled and immutable. You can create up to 10 custom guides for your own workflows.
Skills are detailed instruction sets that teach agents how to work with your knowledge base. The default seeded skills are editable and shared by internal + external agents.
Guides are stored at `~/Library/Application Support/RA-H/guides/` and shared with the main app.
Skills are stored at `~/Library/Application Support/RA-H/skills/` and shared with the main app.
## What's NOT Included
@@ -1,14 +0,0 @@
---
name: Preferences
description: Your communication style, workflow rules, and current priorities.
---
# Preferences
Add your preferences here. The agent will read this guide to understand how you like to work.
## Examples of what to put here
- Communication style (bullet points vs prose, level of detail)
- Workflow rules (always ask before creating dimensions, etc.)
- Current priorities (what you're focused on this week)
- Timezone and locale
@@ -1,38 +0,0 @@
---
name: Creating Nodes
description: When and how to create nodes. Link field rules. Synthesis patterns.
immutable: true
---
# Creating Nodes
## When to Create
- User explicitly asks to save/capture something
- Extracting insights from existing content (synthesis)
- Ingesting external content (YouTube, website, PDF)
## Link Field Rules
- **Has link:** Node directly represents external content (YouTube video, website, PDF, article)
- **No link:** Node is derived/synthesized from existing content (ideas, insights, summaries, questions)
- Never add a link to synthesis or idea nodes
## Synthesis Pattern
When creating a node derived from existing content:
1. Create the node WITHOUT a link field
2. Call `createEdge` to connect it to ALL source nodes
3. Each edge needs an explanation ("Insight extracted from...", "Synthesized from...")
## Dimension Assignment
- New nodes should be assigned to relevant existing dimensions
- Check locked/priority dimensions — these auto-assign
- If no existing dimension fits, create a new one (but prefer existing)
## Description Field
- AI auto-generates a ~1 sentence description after creation
- Description is used for embeddings and search ranking (5x boost)
- Format: what is this node about, in plain language
@@ -1,38 +0,0 @@
---
name: Dimensions
description: Create, lock, describe, organize, clean up dimensions.
immutable: true
---
# Dimensions
Dimensions are how nodes are categorized and organized. Think of them as flexible tags with descriptions.
## Operations
- **Create:** `createDimension(name, description, isPriority)`
- **Update:** `updateDimension(name, { newName, description, isPriority })`
- **Delete:** `deleteDimension(name)` — removes from all nodes
- **Query:** Use sqliteQuery to list dimensions and their node counts
## Locking (Priority)
- `isPriority = true` (locked) → dimension auto-assigns to new nodes when relevant
- `isPriority = false` (unlocked) → manual assignment only
- Lock dimensions that represent active areas of focus
## Naming Conventions
- Lowercase, concise (e.g., "ai", "philosophy", "ra-h")
- Use singular form where natural
- Avoid overlapping names (don't have both "ai" and "artificial-intelligence")
## Description
Every dimension should have a description explaining its purpose. This helps the AI correctly assign nodes to dimensions.
## Cleanup
- Delete dimensions with 0 nodes
- Merge overlapping dimensions (update nodes, then delete the redundant one)
- Regularly review dimension list for coherence
@@ -1,51 +0,0 @@
---
name: Edges
description: Edge philosophy. Explanations, direction, types. Connection patterns.
immutable: true
---
# Edges
## Philosophy
Edges are the most valuable part of the knowledge graph. Individual nodes are useful; the web of connections between them is what makes the graph powerful.
## Rules
1. **Every edge needs an explanation** — why does this connection exist? Be specific.
2. **Direction matters** — FROM → TO should read like a sentence
3. **Types are inferred** — the system infers category/type from your explanation. Don't set types manually.
## Direction Convention
Write the explanation so FROM → TO reads naturally:
- Episode → Podcast: "Episode of this podcast"
- Book → Author: "Written by this author"
- Insight → Source: "Extracted from this source"
- Idea → Related idea: "Builds on this concept"
## Edge Context JSON
```json
{
"explanation": "Human-readable reason",
"category": "inferred (created_by, features, part_of, source_of, related_to)",
"type": "inferred specific type",
"confidence": 0.0-1.0,
"created_via": "chat|mcp|workflow"
}
```
## Hub Traversal
Hub nodes (most-connected) are the user's core themes. To understand context around a topic:
1. Find the relevant hub node
2. Use `queryEdge` or sqliteQuery to get its connections
3. Traverse outward to related nodes
## When to Create Edges
- After creating synthesis/idea nodes (connect to sources)
- When user mentions a relationship between topics
- When running the Connect or Integrate guides
- When obvious connections exist that aren't captured
@@ -1,33 +0,0 @@
---
name: Extract
description: Extraction pre-check. When to reuse chunks vs re-extract.
immutable: true
---
# Content Extraction
## Pre-Check (REQUIRED)
Before running any extraction tool, always check the node first:
1. Call `getNodesById` on the target node
2. Check `chunk_status`:
- **'chunked'** → content already extracted. Reuse existing chunks. Do NOT re-extract.
- **'pending'** or missing → safe to extract
- **'failed'** → previous extraction failed, safe to retry
3. Check if embeddings are available (chunk length > 0)
- If available, use `searchContentEmbeddings` instead of re-extracting
## Extraction Tools
- **youtubeExtract** — YouTube videos (requires URL with video ID)
- **websiteExtract** — Web pages (uses Jina.ai for JS-rendered sites)
- **paperExtract** — PDF files (requires direct PDF URL)
## After Extraction
- The extracted content goes into `chunk` (full source)
- AI generates a `description` (grounding summary)
- Embeddings are created automatically
- Assign to relevant dimensions
@@ -1,111 +0,0 @@
---
name: Schema
description: Full database schema, tables, columns, query patterns.
immutable: true
---
# Database Schema
## Tables
### nodes
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER | Primary key, auto-increment |
| title | TEXT | Required |
| description | TEXT | AI-generated grounding context (~1 sentence) |
| notes | TEXT | User's notes/thoughts (not source content) |
| chunk | TEXT | Full verbatim source content |
| chunk_status | TEXT | 'pending', 'chunked', 'failed' |
| link | TEXT | External URL (only for nodes representing external content) |
| event_date | TEXT | When the content happened (ISO date) — distinct from created_at |
| metadata | TEXT | JSON blob (map_position, transcript_length, etc.) |
| created_at | TEXT | ISO timestamp |
| updated_at | TEXT | ISO timestamp |
### edges
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER | Primary key |
| from_node_id | INTEGER | FK → nodes.id |
| to_node_id | INTEGER | FK → nodes.id |
| context | TEXT | JSON: `{ explanation, category, type, confidence, created_via }` |
| source | TEXT | 'user', 'ai_similarity', or helper name |
| explanation | TEXT | Human-readable reason for connection |
| created_at | TEXT | ISO timestamp |
### dimensions
| Column | Type | Notes |
|--------|------|-------|
| name | TEXT | Primary key |
| description | TEXT | Purpose description |
| icon | TEXT | Emoji or icon identifier for UI display |
| is_priority | INTEGER | 1 = priority dimension (auto-assigns to new nodes) |
| updated_at | TEXT | ISO timestamp |
### node_dimensions (junction)
| Column | Type |
|--------|------|
| node_id | INTEGER FK → nodes.id |
| dimension | TEXT (dimension name) |
### chunks (for semantic search)
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER | Primary key |
| node_id | INTEGER | FK → nodes.id |
| chunk_idx | INTEGER | Position in sequence |
| text | TEXT | Chunk content |
| created_at | TEXT | ISO timestamp |
| embedding_type | TEXT | Embedding model used |
| metadata | TEXT | JSON blob |
### voice_usage (daily tracking)
| Column | Type | Notes |
|--------|------|-------|
| id | INTEGER | Primary key |
| date | TEXT | ISO date (UNIQUE) |
| minutes_used | REAL | Minutes consumed |
| updated_at | TEXT | ISO timestamp |
### FTS Tables
- `chunks_fts` — full-text search on chunk text
- `nodes_fts` — full-text search on node title + notes
## Common Query Patterns
**Top connected nodes (hubs):**
```sql
SELECT n.id, n.title, n.description, COUNT(DISTINCT e.id) AS edge_count
FROM nodes n
LEFT JOIN edges e ON (e.from_node_id = n.id OR e.to_node_id = n.id)
GROUP BY n.id ORDER BY edge_count DESC LIMIT 5
```
**Nodes in a dimension:**
```sql
SELECT n.* FROM nodes n
JOIN node_dimensions nd ON n.id = nd.node_id
WHERE nd.dimension = ?
```
**Edges for a node (both directions):**
```sql
SELECT e.*, n1.title as from_title, n2.title as to_title
FROM edges e
JOIN nodes n1 ON e.from_node_id = n1.id
JOIN nodes n2 ON e.to_node_id = n2.id
WHERE e.from_node_id = ? OR e.to_node_id = ?
```
**Search source content (chunks):**
```sql
SELECT c.id, c.node_id, c.chunk_idx, c.text, n.title
FROM chunks c
JOIN nodes n ON c.node_id = n.id
WHERE c.text LIKE '%search term%' COLLATE NOCASE
ORDER BY c.chunk_idx ASC
LIMIT 10
```
**Use `rah_search_content` to search chunks by keyword, or `rah_sqlite_query` for any read operation not covered by structured tools.**
@@ -1,60 +0,0 @@
---
name: Start Here
description: Master orientation. Read this first when tasks are ambiguous or complex.
immutable: true
---
# Start Here
This is the user's personal knowledge graph — a local SQLite database of nodes connected by edges, organized into dimensions.
## Nodes
Every piece of knowledge is a node. Key fields:
- **title** — Clear, descriptive, max 160 chars. Should stand alone as a meaningful label.
- **description** — One-sentence summary. Powers search and AI understanding. Auto-generated if omitted.
- **content** — Your notes, analysis, commentary. This is where synthesis lives.
- **chunk** — Verbatim source text (transcript, article body, full quote). Kept separate from your analysis in content.
- **link** — ONLY for external content (URLs, YouTube, articles, PDFs). Omit for ideas, synthesis, or anything derived from existing nodes.
- **dimensions** — 15 categories. Always call rah_list_dimensions first and use existing ones.
Two types of nodes:
- **External content** (has link) — articles, videos, papers, tweets. The link points to the source.
- **Synthesis/ideas** (no link) — insights, connections, personal thinking. Create edges back to source nodes instead.
## Edges
Connections between nodes. The most valuable part of the graph — they represent understanding, not proximity.
- Every edge needs an **explanation** — a human-readable sentence explaining WHY this connection exists
- **Direction matters** — reads as: source → [explanation] → target. Example: node "Alice" → "invented this technique" → node "Technique X"
- Edge types are inferred from the explanation — don't set them manually
- Create edges after synthesis, when relationships surface, or when a guide instructs you to
## Dimensions
Categories that organize nodes. Two kinds:
- **Priority (locked)** — core categories the user actively maintains (e.g., "research", "person", "idea", "entity")
- **Non-priority** — secondary tags applied when relevant (e.g., "philosophy", "venture", "ai")
- Naming: lowercase, singular form, concise, no overlapping names
- Every dimension needs a description explaining its purpose
- Check existing dimensions before creating new ones — avoid near-duplicates
## Key Conventions
1. **Search before creating** — always call rah_search_nodes to check for duplicates
2. **Content appends, dimensions replace** — when updating a node, new content is added below existing content; dimensions are fully replaced with the new array
3. **Hub nodes** are the most-connected nodes — they represent the user's major themes and interests
4. **Proactively save valuable information** — when you spot something worth preserving in a conversation, offer to add it
5. **Quality over quantity** — one well-titled, well-connected node is better than five shallow ones
## Guides
There are two kinds of guides available via rah_list_guides:
- **System guides (immutable)** — reference documentation maintained by RA-H. Cover schema, node creation, edges, dimensions, and content extraction. Always available, cannot be modified.
- **User guides (custom)** — created by the user for their own workflows, preferences, and procedures. Can be created, edited, and deleted via rah_write_guide / rah_delete_guide.
Call rah_list_guides to see what's available, then rah_read_guide to load any guide you need.
+152 -104
View File
@@ -32,40 +32,55 @@ const { initDatabase, getDatabasePath, closeDatabase, getDb, query } = require('
const nodeService = require('./services/nodeService');
const edgeService = require('./services/edgeService');
const dimensionService = require('./services/dimensionService');
const guideService = require('./services/guideService');
const skillService = require('./services/skillService');
// Server info
const serverInfo = {
name: 'ra-h-standalone',
version: '1.4.1'
version: '1.7.0'
};
function buildInstructions() {
const now = new Date().toISOString().split('T')[0];
return [
`Today's date: ${now}.`,
"RA-H is the user's personal knowledge graph — local SQLite, fully on-device.",
'Call rah_get_context first for a quick orientation (stats, hubs, dimensions).',
'For simple tasks (add a node, search), the tool descriptions have everything you need — just execute.',
'For complex or ambiguous tasks, also call rah_read_guide("start-here") for full graph understanding.',
'Knowledge capture: after substantive exchanges, offer to save valuable information.',
'Triggers: a new insight emerges, a decision is made, a person/entity/concept is discussed in depth, research or references are shared, a connection to existing knowledge surfaces.',
'When offering, propose a specific node — title, dimensions, and a concrete description (WHAT it is + WHY it matters, no vague "discusses/explores") — so the user can approve with minimal friction.',
'Don\'t ask "should I save this?" — instead say "I\'d add this as: [title] in [dimensions] — want me to?"',
'Search before creating to avoid duplicates.',
'All data stays on this device.'
].join(' ');
}
let skillIndex = '- db-operations: Core graph read/write operating policy.';
const instructions = buildInstructions();
try {
const skills = skillService.listSkills();
if (Array.isArray(skills) && skills.length > 0) {
skillIndex = skills
.map(s => `- ${s.name}: ${s.description}`)
.join('\n');
}
} catch (error) {
log('Warning: failed to load skill index for instructions:', error.message);
}
return `Today's date: ${now}. RA-H is the user's personal knowledge graph — local SQLite, fully on-device.
## Quick start
1. Call getContext for orientation (stats, hubs, dimensions).
2. For simple tasks, tool descriptions have everything you need.
3. For complex tasks, call readSkill("db-operations").
## Knowledge capture
Proactively offer to save valuable information when insights, decisions, or references surface.
Propose: "I'd add this as: [title] in [dimensions] — want me to?"
Always search before creating to avoid duplicates.
## Available skills
${skillIndex}
Load any skill with readSkill("name").
All data stays on this device.`;
}
// Tool schemas
const addNodeInputSchema = {
title: z.string().min(1).max(160).describe('Clear, descriptive title'),
content: z.string().max(20000).optional().describe('Node content/notes'),
link: z.string().url().optional().describe('Source URL'),
description: z.string().max(2000).optional().describe('One-sentence summary: WHAT this is (explicit, concrete) + WHY it matters. No weak verbs (discusses, explores, examines). Example: "Podcast — Lex Fridman interviews Sam Altman on AGI timelines. First public comments since board drama."'),
dimensions: z.array(z.string()).min(1).max(5).describe('1-5 categories. Call rah_list_dimensions first to use existing ones.'),
description: z.string().min(24).max(280).describe('REQUIRED. One-sentence summary: WHAT this is (explicit, concrete) + WHY it matters. No weak verbs (discusses, explores, examines). Example: "Podcast — Lex Fridman interviews Sam Altman on AGI timelines. First public comments since board drama."'),
dimensions: z.array(z.string()).min(1).max(5).describe('1-5 categories. Call queryDimensions first to use existing ones.'),
metadata: z.record(z.any()).optional().describe('Additional metadata'),
chunk: z.string().max(50000).optional().describe('Full source text')
};
@@ -88,7 +103,7 @@ const updateNodeInputSchema = {
id: z.number().int().positive().describe('Node ID'),
updates: z.object({
title: z.string().optional().describe('New title'),
description: z.string().optional().describe('New description (overwrites existing)'),
description: z.string().min(24).max(280).describe('REQUIRED. Explicitly state WHAT this is (podcast, conversation summary, user insight, etc.) + WHY it matters for context grounding. No vague verbs like "discusses/explores/examines".'),
content: z.string().optional().describe('Content to APPEND'),
link: z.string().optional().describe('New link'),
dimensions: z.array(z.string()).optional().describe('New dimensions (replaces existing)'),
@@ -131,17 +146,17 @@ const deleteDimensionInputSchema = {
name: z.string().min(1).describe('Dimension name to delete')
};
const readGuideInputSchema = {
name: z.string().min(1).describe('Guide name (e.g. "edges", "creating-nodes", "schema")')
const readSkillInputSchema = {
name: z.string().min(1).describe('Skill name (e.g. "db-operations", "onboarding", "persona")')
};
const writeGuideInputSchema = {
name: z.string().min(1).describe('Guide name (lowercase, no spaces)'),
const writeSkillInputSchema = {
name: z.string().min(1).describe('Skill name (lowercase, no spaces)'),
content: z.string().min(1).describe('Full markdown content including YAML frontmatter (name, description)')
};
const deleteGuideInputSchema = {
name: z.string().min(1).describe('Guide name to delete')
const deleteSkillInputSchema = {
name: z.string().min(1).describe('Skill name to delete')
};
const searchContentInputSchema = {
@@ -173,6 +188,21 @@ function sanitizeDimensions(raw) {
return result;
}
function validateExplicitDescription(description) {
if (typeof description !== 'string') {
return 'Description is required and must be a string.';
}
const text = description.trim();
if (text.length < 24) {
return 'Description must be explicit and substantial (at least 24 characters).';
}
const weakPatterns = /\b(discusses|explores|examines|talks about|is about|delves into)\b/i;
if (weakPatterns.test(text)) {
return 'Description is too vague. State exactly what this is and why it matters.';
}
return null;
}
// FTS5 helpers
function sanitizeFtsQuery(input) {
return input
@@ -267,21 +297,27 @@ async function main() {
process.exit(1);
}
const instructions = buildInstructions();
const server = new McpServer(serverInfo, { instructions });
function registerToolWithAliases(name, config, handler) {
server.registerTool(name, config, handler);
}
// ========== CONTEXT TOOL ==========
server.registerTool(
'rah_get_context',
registerToolWithAliases(
'getContext',
{
title: 'Get RA-H context',
description: 'Get knowledge graph overview: stats, hub nodes (most connected), dimensions, recent activity, and available guides. Call this first to orient yourself. For deeper understanding, follow up with rah_read_guide("start-here").',
description: 'Get knowledge graph overview: stats, hub nodes (most connected), dimensions, recent activity, and available skills. Call this first to orient yourself. For deeper operating policy, follow up with readSkill("db-operations").',
inputSchema: {}
},
async () => {
const context = nodeService.getContext();
const guides = guideService.listGuides();
context.guides = guides.map(g => ({ name: g.name, description: g.description, immutable: g.immutable }));
const skills = skillService.listSkills();
context.skills = skills.map(s => ({ name: s.name, description: s.description, immutable: s.immutable }));
context.guides = context.skills;
// First-run welcome message
if (context.stats.nodeCount === 0) {
@@ -295,7 +331,7 @@ async function main() {
};
}
const summary = `Graph: ${context.stats.nodeCount} nodes, ${context.stats.edgeCount} edges, ${context.stats.dimensionCount} dimensions, ${guides.length} guides.`;
const summary = `Graph: ${context.stats.nodeCount} nodes, ${context.stats.edgeCount} edges, ${context.stats.dimensionCount} dimensions, ${skills.length} skills.`;
return {
content: [{ type: 'text', text: summary }],
structuredContent: context
@@ -305,11 +341,11 @@ async function main() {
// ========== NODE TOOLS ==========
server.registerTool(
'rah_add_node',
registerToolWithAliases(
'createNode',
{
title: 'Add RA-H node',
description: 'Create a new node. Always search first (rah_search_nodes) to avoid duplicates. Title: max 160 chars, clear and descriptive. Use "link" ONLY for external content (URL, video, article) — omit for synthesis/ideas derived from existing nodes. "content" = your notes/analysis. "chunk" = verbatim source text. "description" = one-sentence summary for search. Assign 1-5 dimensions — call rah_list_dimensions first to use existing ones.',
description: 'Create a new node. Always search first (queryNodes) to avoid duplicates. Title: max 160 chars, clear and descriptive. Description is REQUIRED and must be explicit about what the thing is and why it matters for contextual grounding. Use "link" ONLY for external content (URL, video, article) — omit for synthesis/ideas derived from existing nodes. "content" = your notes/analysis. "chunk" = verbatim source text. Assign 1-5 dimensions — call queryDimensions first to use existing ones.',
// Note: MCP schema uses "content" for external API compat; mapped to "notes" internally
inputSchema: addNodeInputSchema
},
@@ -318,6 +354,10 @@ async function main() {
if (normalizedDimensions.length === 0) {
throw new Error('At least one dimension is required.');
}
const descriptionError = validateExplicitDescription(description);
if (descriptionError) {
throw new Error(descriptionError);
}
const node = nodeService.createNode({
title: title.trim(),
@@ -343,11 +383,11 @@ async function main() {
}
);
server.registerTool(
'rah_search_nodes',
registerToolWithAliases(
'queryNodes',
{
title: 'Search RA-H nodes',
description: 'Search nodes by keyword across title, description, and content fields. Multi-word queries find nodes containing all words (not exact phrases). Returns up to 25 results (default 10). Call before creating nodes to check for duplicates. Optionally filter by dimensions.',
description: 'Search nodes by keyword across title, description, and content fields. Multi-word queries find nodes containing all words (not exact phrases). Returns up to 25 results (default 10). Call before creating nodes to check for duplicates. Optionally filter by dimensions. NOT for searching source documents (transcripts, articles) — use searchContentEmbeddings for that.',
// Note: searches across title, description, and notes columns
inputSchema: searchNodesInputSchema
},
@@ -497,11 +537,11 @@ async function main() {
}
);
server.registerTool(
'rah_get_nodes',
registerToolWithAliases(
'getNodesById',
{
title: 'Get RA-H nodes by ID',
description: 'Load full node records by their IDs (max 10 per call). Chunks over 10K chars are truncated — check chunk_truncated and chunk_length fields. For full text, use rah_search_content to search or rah_sqlite_query with substr() to read sections.',
description: 'Load full node records by their IDs (max 10 per call). Chunks over 10K chars are truncated — check chunk_truncated and chunk_length fields. For full text, use searchContentEmbeddings to search or sqliteQuery with substr() to read sections.',
inputSchema: getNodesInputSchema
},
async ({ nodeIds }) => {
@@ -546,17 +586,24 @@ async function main() {
}
);
server.registerTool(
'rah_update_node',
registerToolWithAliases(
'updateNode',
{
title: 'Update RA-H node',
description: 'Update an existing node. Content is APPENDED to existing notes (not replaced). Dimensions are REPLACED entirely with the new array. Title, description, and link are overwritten.',
description: 'Update an existing node. Description is REQUIRED on every update and must explicitly state WHAT this thing is + WHY it matters for contextual grounding. Content is APPENDED to existing notes (not replaced). Dimensions are REPLACED entirely with the new array. Title, description, and link are overwritten. Call getNodesById first to verify current state before updating.',
inputSchema: updateNodeInputSchema
},
async ({ id, updates }) => {
if (!updates || Object.keys(updates).length === 0) {
throw new Error('At least one field must be provided in updates.');
}
if (!updates.description) {
throw new Error('Every node update requires an explicit description (WHAT this is + WHY it matters).');
}
const descriptionError = validateExplicitDescription(updates.description);
if (descriptionError) {
throw new Error(descriptionError);
}
// Map MCP 'content' field → internal 'notes' field
const mappedUpdates = { ...updates };
@@ -580,11 +627,11 @@ async function main() {
// ========== EDGE TOOLS ==========
server.registerTool(
'rah_create_edge',
registerToolWithAliases(
'createEdge',
{
title: 'Create RA-H edge',
description: 'Connect two nodes with an edge. Edges are the most valuable part of the graph — they represent understanding, not proximity. Direction matters: reads as sourceId → [explanation] → targetId. The explanation should read as a sentence (e.g. "invented this technique", "contradicts the claim in").',
description: 'Connect two nodes with an edge. Edges are the most valuable part of the graph — they represent understanding, not proximity. Direction matters: reads as sourceId → [explanation] → targetId. The explanation should read as a sentence (e.g. "invented this technique", "contradicts the claim in"). Call queryEdge first to check if a connection already exists between the two nodes.',
inputSchema: createEdgeInputSchema
},
async ({ sourceId, targetId, explanation }) => {
@@ -606,8 +653,8 @@ async function main() {
}
);
server.registerTool(
'rah_update_edge',
registerToolWithAliases(
'updateEdge',
{
title: 'Update RA-H edge',
description: 'Update an edge explanation. Use when a connection needs a better or corrected explanation.',
@@ -627,11 +674,11 @@ async function main() {
}
);
server.registerTool(
'rah_query_edges',
registerToolWithAliases(
'queryEdge',
{
title: 'Query RA-H edges',
description: 'Find edges/connections. Optionally filter by nodeId to see all connections for a specific node. Returns up to 50 edges (default 25) with edge IDs, connected node IDs, and explanations.',
description: 'Find edges/connections. Optionally filter by nodeId to see all connections for a specific node. Returns up to 50 edges (default 25) with edge IDs, connected node IDs, and explanations. Use when exploring how nodes relate, checking for existing connections before creating edges, or traversing the graph from a hub node.',
inputSchema: queryEdgesInputSchema
},
async ({ nodeId, limit = 25 }) => {
@@ -658,11 +705,11 @@ async function main() {
// ========== DIMENSION TOOLS ==========
server.registerTool(
'rah_list_dimensions',
registerToolWithAliases(
'queryDimensions',
{
title: 'List RA-H dimensions',
description: 'Get all dimensions with node counts.',
description: 'Get all dimensions with node counts. Call before creating nodes (to assign existing dimensions) or before creating new dimensions (to avoid duplicates). Shows priority/locked status.',
inputSchema: listDimensionsInputSchema
},
async () => {
@@ -678,8 +725,8 @@ async function main() {
}
);
server.registerTool(
'rah_create_dimension',
registerToolWithAliases(
'createDimension',
{
title: 'Create RA-H dimension',
description: 'Create a new dimension/category. Use lowercase, singular form (e.g. "biology" not "Biology" or "biologies"). Set isPriority=true to lock it for automatic assignment to new nodes. Always include a description.',
@@ -703,8 +750,8 @@ async function main() {
}
);
server.registerTool(
'rah_update_dimension',
registerToolWithAliases(
'updateDimension',
{
title: 'Update RA-H dimension',
description: 'Update or rename a dimension.',
@@ -730,11 +777,11 @@ async function main() {
}
);
server.registerTool(
'rah_delete_dimension',
registerToolWithAliases(
'deleteDimension',
{
title: 'Delete RA-H dimension',
description: 'Delete a dimension and remove it from all nodes.',
description: 'Delete a dimension and remove it from all nodes. WARNING: This is destructive — the dimension will be removed from ALL nodes that use it. Consider checking node counts with queryDimensions first.',
inputSchema: deleteDimensionInputSchema
},
async ({ name }) => {
@@ -750,94 +797,95 @@ async function main() {
}
);
// ========== GUIDE TOOLS ==========
// ========== SKILL TOOLS ==========
server.registerTool(
'rah_list_guides',
registerToolWithAliases(
'listSkills',
{
title: 'List RA-H guides',
description: 'List all guides — system guides (immutable reference docs) and user-created guides (custom workflows and preferences). Read "start-here" first for orientation.',
title: 'List RA-H skills',
description: 'List all skills available in this workspace. Read "db-operations" first for core graph operating policy.',
inputSchema: {}
},
async () => {
const guides = guideService.listGuides();
const skills = skillService.listSkills();
return {
content: [{ type: 'text', text: `Found ${guides.length} guide(s).` }],
content: [{ type: 'text', text: `Found ${skills.length} skill(s).` }],
structuredContent: {
count: guides.length,
guides
count: skills.length,
skills,
guides: skills
}
};
}
);
server.registerTool(
'rah_read_guide',
registerToolWithAliases(
'readSkill',
{
title: 'Read RA-H guide',
description: 'Read a guide by name. Returns full markdown with procedural instructions. Read "start-here" for master orientation. Call rah_list_guides to see all available guides.',
inputSchema: readGuideInputSchema
title: 'Read RA-H skill',
description: 'Read a skill by name. Returns full markdown with procedural instructions. Read "db-operations" for core policy. Call listSkills to see all available skills.',
inputSchema: readSkillInputSchema
},
async ({ name }) => {
const guide = guideService.readGuide(name);
const skill = skillService.readSkill(name);
if (!guide) {
throw new Error(`Guide "${name}" not found. Call rah_list_guides to see available guides.`);
if (!skill) {
throw new Error(`Skill "${name}" not found. Call listSkills to see available skills.`);
}
return {
content: [{ type: 'text', text: guide.content }],
structuredContent: guide
content: [{ type: 'text', text: skill.content }],
structuredContent: skill
};
}
);
server.registerTool(
'rah_write_guide',
registerToolWithAliases(
'writeSkill',
{
title: 'Write RA-H guide',
description: 'Create or update a custom guide. System guides cannot be modified. Content should be markdown with YAML frontmatter (name, description).',
inputSchema: writeGuideInputSchema
title: 'Write RA-H skill',
description: 'Create or update a skill. Content should be markdown with YAML frontmatter (name, description).',
inputSchema: writeSkillInputSchema
},
async ({ name, content }) => {
const result = guideService.writeGuide(name, content);
const result = skillService.writeSkill(name, content);
if (!result.success) {
throw new Error(result.error);
}
return {
content: [{ type: 'text', text: `Guide "${name}" saved.` }],
content: [{ type: 'text', text: `Skill "${name}" saved.` }],
structuredContent: {
success: true,
name,
message: `Guide "${name}" saved.`
message: `Skill "${name}" saved.`
}
};
}
);
server.registerTool(
'rah_delete_guide',
registerToolWithAliases(
'deleteSkill',
{
title: 'Delete RA-H guide',
description: 'Delete a custom guide. System guides cannot be deleted.',
inputSchema: deleteGuideInputSchema
title: 'Delete RA-H skill',
description: 'Delete a skill.',
inputSchema: deleteSkillInputSchema
},
async ({ name }) => {
const result = guideService.deleteGuide(name);
const result = skillService.deleteSkill(name);
if (!result.success) {
throw new Error(result.error);
}
return {
content: [{ type: 'text', text: `Guide "${name}" deleted.` }],
content: [{ type: 'text', text: `Skill "${name}" deleted.` }],
structuredContent: {
success: true,
name,
message: `Guide "${name}" deleted.`
message: `Skill "${name}" deleted.`
}
};
}
@@ -845,11 +893,11 @@ async function main() {
// ========== CONTENT SEARCH TOOL ==========
server.registerTool(
'rah_search_content',
registerToolWithAliases(
'searchContentEmbeddings',
{
title: 'Search RA-H source content',
description: 'Search through source content (transcripts, books, articles) stored as chunks. Use when you need to find specific text within a node\'s full source material. For node-level search (titles, descriptions), use rah_search_nodes instead.',
description: 'Search through source content (transcripts, books, articles) stored as chunks. Use when you need to find specific text within a node\'s full source material. For node-level search (titles, descriptions), use queryNodes instead.',
inputSchema: searchContentInputSchema
},
async ({ query: searchQuery, node_id, limit = 5 }) => {
@@ -859,7 +907,7 @@ async function main() {
const tableCheck = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chunks'").get();
if (!tableCheck) {
return {
content: [{ type: 'text', text: 'No chunks table found. Source content has not been chunked yet. Use rah_get_nodes to read the raw chunk field instead.' }],
content: [{ type: 'text', text: 'No chunks table found. Source content has not been chunked yet. Use getNodesById to read the raw chunk field instead.' }],
structuredContent: { count: 0, chunks: [], note: 'chunks table does not exist' }
};
}
@@ -960,11 +1008,11 @@ async function main() {
// ========== SQL QUERY TOOL ==========
server.registerTool(
'rah_sqlite_query',
registerToolWithAliases(
'sqliteQuery',
{
title: 'Execute read-only SQL',
description: 'Execute read-only SQL queries against the knowledge graph database. Tables: nodes, edges, dimensions, node_dimensions, chunks. Use PRAGMA table_info(tablename) for schema. Only SELECT/WITH/PRAGMA allowed.',
description: 'Execute read-only SQL queries against the knowledge graph database. Tables: nodes, edges, dimensions, node_dimensions, chunks. Use PRAGMA table_info(tablename) for schema. Only SELECT/WITH/PRAGMA allowed. Use when structured tools are insufficient — e.g., complex JOINs, aggregations, or custom filtering. Read readSkill("schema") for table definitions and query patterns.',
inputSchema: sqliteQueryInputSchema
},
async ({ sql: userSql, format = 'json' }) => {
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "ra-h-mcp-server",
"version": "1.4.2",
"version": "1.8.0",
"description": "Connect Claude Code/Desktop to your RA-H knowledge base. Direct SQLite access - no web app required.",
"main": "index.js",
"bin": {
@@ -43,7 +43,7 @@
"files": [
"index.js",
"services/",
"guides/",
"skills/",
"README.md"
]
}
@@ -15,6 +15,7 @@ function getDimensions() {
SELECT
d.name AS dimension,
d.description,
d.icon,
d.is_priority AS isPriority,
COALESCE(dc.count, 0) AS count
FROM dimensions d
@@ -27,6 +28,7 @@ function getDimensions() {
return rows.map(row => ({
dimension: row.dimension,
description: row.description,
icon: row.icon || null,
isPriority: Boolean(row.isPriority),
count: Number(row.count)
}));
@@ -99,7 +101,7 @@ function updateDimension(data) {
const dimResult = dimStmt.run(newName, currentName);
if (dimResult.changes === 0) {
throw new Error('Dimension not found');
throw new Error('Dimension not found. Use rah_list_dimensions to see all dimensions.');
}
// Update node_dimensions
@@ -138,7 +140,7 @@ function updateDimension(data) {
}
if (updates.length === 0) {
throw new Error('At least one update field must be provided');
throw new Error('At least one update field must be provided (description, isPriority, or newName).');
}
updates.push('updated_at = CURRENT_TIMESTAMP');
@@ -153,7 +155,7 @@ function updateDimension(data) {
const result = stmt.run(...params);
if (result.changes === 0) {
throw new Error('Dimension not found');
throw new Error('Dimension not found. Use rah_list_dimensions to see all dimensions.');
}
return {
@@ -184,7 +186,7 @@ function deleteDimension(name) {
});
if (!removal.removedLinks && !removal.removedRow) {
throw new Error('Dimension not found');
throw new Error('Dimension not found. Use rah_list_dimensions to see all dimensions.');
}
return {
@@ -95,7 +95,7 @@ function updateEdge(id, updates) {
const existing = getEdgeById(id);
if (!existing) {
throw new Error(`Edge with ID ${id} not found`);
throw new Error(`Edge with ID ${id} not found. Use rah_query_edges to find edges by node ID.`);
}
// If explanation changed, update context
@@ -128,7 +128,7 @@ function updateEdge(id, updates) {
function deleteEdge(id) {
const result = query('DELETE FROM edges WHERE id = ?', [id]);
if (result.changes === 0) {
throw new Error(`Edge with ID ${id} not found`);
throw new Error(`Edge with ID ${id} not found. Use rah_query_edges to find edges by node ID.`);
}
return true;
}
@@ -1,204 +1,10 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const MAX_USER_GUIDES = 10;
// Where guides live on disk (shared with the app)
const GUIDES_DIR = path.join(
os.homedir(),
'Library', 'Application Support', 'RA-H', 'guides'
);
// System guides bundled with this package
const BUNDLED_SYSTEM_DIR = path.join(__dirname, '..', 'guides', 'system');
const BUNDLED_USER_DIR = path.join(__dirname, '..', 'guides');
// System guide names (immutable, always re-seeded)
const SYSTEM_GUIDE_NAMES = new Set([
'start-here',
'schema',
'creating-nodes',
'edges',
'dimensions',
'extract',
]);
/**
* Parse YAML frontmatter from markdown without external deps.
* Returns { data: {}, content: string }
*/
function parseFrontmatter(raw) {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) return { data: {}, content: raw.trim() };
const yamlBlock = match[1];
const content = match[2];
const data = {};
for (const line of yamlBlock.split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
let value = line.slice(colonIdx + 1).trim();
// Handle booleans
if (value === 'true') value = true;
else if (value === 'false') value = false;
data[key] = value;
}
return { data, content: content.trim() };
}
function isSystemGuide(filename) {
const name = filename.replace('.md', '');
return SYSTEM_GUIDE_NAMES.has(name);
}
function ensureGuidesDir() {
if (!fs.existsSync(GUIDES_DIR)) {
fs.mkdirSync(GUIDES_DIR, { recursive: true });
}
}
/**
* Seed guides on first run.
* System guides always overwrite. User guides only seed if missing.
*/
function seedGuides() {
ensureGuidesDir();
// Always re-seed system guides (immutable)
if (fs.existsSync(BUNDLED_SYSTEM_DIR)) {
const files = fs.readdirSync(BUNDLED_SYSTEM_DIR).filter(f => f.endsWith('.md'));
for (const file of files) {
fs.copyFileSync(path.join(BUNDLED_SYSTEM_DIR, file), path.join(GUIDES_DIR, file));
}
}
// Seed default user guides only if they don't exist
if (fs.existsSync(BUNDLED_USER_DIR)) {
const files = fs.readdirSync(BUNDLED_USER_DIR).filter(f => f.endsWith('.md'));
for (const file of files) {
const dest = path.join(GUIDES_DIR, file);
if (!fs.existsSync(dest)) {
fs.copyFileSync(path.join(BUNDLED_USER_DIR, file), dest);
}
}
}
}
let initialized = false;
function init() {
if (initialized) return;
seedGuides();
initialized = true;
}
/**
* List all guides with name, description, immutable flag.
*/
function listGuides() {
init();
if (!fs.existsSync(GUIDES_DIR)) return [];
const files = fs.readdirSync(GUIDES_DIR).filter(f => f.endsWith('.md'));
const guides = files.map(file => {
const raw = fs.readFileSync(path.join(GUIDES_DIR, file), 'utf-8');
const { data } = parseFrontmatter(raw);
const immutable = isSystemGuide(file) || data.immutable === true;
return {
name: data.name || file.replace('.md', ''),
description: data.description || '',
immutable,
};
});
// System guides first, then user guides alphabetically
return guides.sort((a, b) => {
if (a.immutable && !b.immutable) return -1;
if (!a.immutable && b.immutable) return 1;
return a.name.localeCompare(b.name);
});
}
/**
* Read a guide by name. Returns full content.
*/
function readGuide(name) {
init();
const candidates = [`${name}.md`, `${name.toLowerCase()}.md`];
for (const filename of candidates) {
const filepath = path.join(GUIDES_DIR, filename);
if (fs.existsSync(filepath)) {
const raw = fs.readFileSync(filepath, 'utf-8');
const { data, content } = parseFrontmatter(raw);
const immutable = isSystemGuide(filename) || data.immutable === true;
return {
name: data.name || name,
description: data.description || '',
immutable,
content,
};
}
}
return null;
}
/**
* Write or update a guide. Rejects writes to system guides.
*/
function writeGuide(name, content) {
init();
const filename = `${name.toLowerCase()}.md`;
if (isSystemGuide(filename)) {
return { success: false, error: `Guide "${name}" is a system guide and cannot be modified.` };
}
const filepath = path.join(GUIDES_DIR, filename);
if (!fs.existsSync(filepath)) {
const userCount = listGuides().filter(g => !g.immutable).length;
if (userCount >= MAX_USER_GUIDES) {
return { success: false, error: `Maximum of ${MAX_USER_GUIDES} custom guides reached. Delete a guide first.` };
}
}
ensureGuidesDir();
fs.writeFileSync(filepath, content, 'utf-8');
return { success: true };
}
/**
* Delete a guide. Rejects deletes of system guides.
*/
function deleteGuide(name) {
init();
const candidates = [`${name}.md`, `${name.toLowerCase()}.md`];
for (const filename of candidates) {
if (isSystemGuide(filename)) {
return { success: false, error: `Guide "${name}" is a system guide and cannot be deleted.` };
}
const filepath = path.join(GUIDES_DIR, filename);
if (fs.existsSync(filepath)) {
fs.unlinkSync(filepath);
return { success: true };
}
}
return { success: false, error: `Guide "${name}" not found.` };
}
const skillService = require('./skillService');
module.exports = {
listGuides,
readGuide,
writeGuide,
deleteGuide,
listGuides: skillService.listSkills,
readGuide: skillService.readSkill,
writeGuide: skillService.writeSkill,
deleteGuide: skillService.deleteSkill,
};
@@ -180,7 +180,7 @@ function updateNode(id, updates, options = {}) {
// Check node exists
const existing = getNodeById(id);
if (!existing) {
throw new Error(`Node with ID ${id} not found`);
throw new Error(`Node with ID ${id} not found. Use rah_search_nodes to find nodes by keyword.`);
}
transaction(() => {
@@ -251,7 +251,7 @@ function updateNode(id, updates, options = {}) {
function deleteNode(id) {
const result = query('DELETE FROM nodes WHERE id = ?', [id]);
if (result.changes === 0) {
throw new Error(`Node with ID ${id} not found`);
throw new Error(`Node with ID ${id} not found. Use rah_search_nodes to find nodes by keyword.`);
}
return true;
}
@@ -0,0 +1,276 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const SKILLS_DIR = path.join(
os.homedir(),
'Library', 'Application Support', 'RA-H', 'skills'
);
const LEGACY_GUIDES_DIR = path.join(
os.homedir(),
'Library', 'Application Support', 'RA-H', 'guides'
);
const BUNDLED_SKILLS_DIR = path.join(__dirname, '..', 'skills');
const SEED_MIGRATION_FLAG = path.join(SKILLS_DIR, '.seed-migrated-2026-03-07-skills-overhaul');
const SEEDED_SKILL_IDS = new Set([
'db-operations',
'create-skill',
'audit',
'traverse',
'onboarding',
'persona',
'calibration',
'connect',
]);
const DEPRECATED_SKILL_IDS = new Set([
'start-here',
'schema',
'creating-nodes',
'edges',
'dimensions',
'extract',
'troubleshooting',
'integrate',
'test-guide',
'ghostwriting-brad',
'write-the-debrief',
'prep',
'preferences',
'research',
'survey',
'traverse-graph',
]);
function parseFrontmatter(raw) {
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) return { data: {}, content: raw.trim() };
const yamlBlock = match[1];
const content = match[2];
const data = {};
for (const line of yamlBlock.split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
let value = line.slice(colonIdx + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (value === 'true') value = true;
else if (value === 'false') value = false;
data[key] = value;
}
return { data, content: content.trim() };
}
function stripMdExtension(value) {
return value.replace(/\.md$/i, '');
}
function normalizeSkillId(value) {
return stripMdExtension(value)
.trim()
.toLowerCase()
.replace(/[\s_]+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
function listMarkdownFiles(dir) {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
}
function ensureSkillsDir() {
if (!fs.existsSync(SKILLS_DIR)) {
fs.mkdirSync(SKILLS_DIR, { recursive: true });
}
}
function migrateLegacyGuides() {
const files = listMarkdownFiles(LEGACY_GUIDES_DIR);
for (const file of files) {
const dest = path.join(SKILLS_DIR, file);
if (!fs.existsSync(dest)) {
fs.copyFileSync(path.join(LEGACY_GUIDES_DIR, file), dest);
}
}
}
function seedSkills() {
ensureSkillsDir();
const bundledFiles = listMarkdownFiles(BUNDLED_SKILLS_DIR);
const bundledById = new Map();
for (const file of bundledFiles) {
bundledById.set(normalizeSkillId(file), path.join(BUNDLED_SKILLS_DIR, file));
}
for (const skillId of SEEDED_SKILL_IDS) {
const source = bundledById.get(skillId);
if (!source) continue;
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
if (!fs.existsSync(dest)) {
fs.copyFileSync(source, dest);
}
}
}
function migrateSeededBaseline() {
if (fs.existsSync(SEED_MIGRATION_FLAG)) {
return;
}
const bundledFiles = listMarkdownFiles(BUNDLED_SKILLS_DIR);
const bundledById = new Map();
for (const file of bundledFiles) {
bundledById.set(normalizeSkillId(file), path.join(BUNDLED_SKILLS_DIR, file));
}
for (const skillId of SEEDED_SKILL_IDS) {
const source = bundledById.get(skillId);
if (!source) continue;
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
fs.copyFileSync(source, dest);
}
fs.writeFileSync(SEED_MIGRATION_FLAG, 'ok', 'utf-8');
}
function pruneDeprecatedSkills() {
const files = listMarkdownFiles(SKILLS_DIR);
for (const file of files) {
const normalized = normalizeSkillId(file);
if (DEPRECATED_SKILL_IDS.has(normalized)) {
fs.unlinkSync(path.join(SKILLS_DIR, file));
}
}
}
function resolveSkillFilename(name) {
if (!fs.existsSync(SKILLS_DIR)) {
return null;
}
const files = fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md'));
const normalizedInput = normalizeSkillId(name);
const directCandidates = [
`${name}.md`,
`${name.toLowerCase()}.md`,
normalizedInput ? `${normalizedInput}.md` : '',
].filter(Boolean);
for (const candidate of directCandidates) {
if (files.includes(candidate)) {
return candidate;
}
}
for (const file of files) {
if (normalizeSkillId(file) === normalizedInput) {
return file;
}
const raw = fs.readFileSync(path.join(SKILLS_DIR, file), 'utf-8');
const { data } = parseFrontmatter(raw);
if (typeof data.name === 'string' && normalizeSkillId(data.name) === normalizedInput) {
return file;
}
}
return null;
}
let initialized = false;
function init() {
if (initialized) return;
ensureSkillsDir();
migrateLegacyGuides();
migrateSeededBaseline();
seedSkills();
pruneDeprecatedSkills();
initialized = true;
}
function listSkills() {
init();
if (!fs.existsSync(SKILLS_DIR)) return [];
const files = fs.readdirSync(SKILLS_DIR).filter((f) => f.endsWith('.md'));
const skills = files.map((file) => {
const raw = fs.readFileSync(path.join(SKILLS_DIR, file), 'utf-8');
const { data } = parseFrontmatter(raw);
return {
name: data.name || file.replace('.md', ''),
description: data.description || '',
immutable: false,
};
});
return skills.sort((a, b) => a.name.localeCompare(b.name));
}
function readSkill(name) {
init();
const filename = resolveSkillFilename(name);
if (!filename) {
return null;
}
const filepath = path.join(SKILLS_DIR, filename);
const raw = fs.readFileSync(filepath, 'utf-8');
const { data, content } = parseFrontmatter(raw);
return {
name: data.name || stripMdExtension(filename),
description: data.description || '',
immutable: false,
content,
};
}
function writeSkill(name, content) {
init();
const normalizedName = normalizeSkillId(name);
const existingFilename = resolveSkillFilename(name);
const filename = existingFilename || `${normalizedName || name.toLowerCase()}.md`;
const filepath = path.join(SKILLS_DIR, filename);
ensureSkillsDir();
fs.writeFileSync(filepath, content, 'utf-8');
return { success: true };
}
function deleteSkill(name) {
init();
const filename = resolveSkillFilename(name);
if (!filename) {
return { success: false, error: `Skill "${name}" not found.` };
}
const filepath = path.join(SKILLS_DIR, filename);
fs.unlinkSync(filepath);
return { success: true };
}
module.exports = {
listSkills,
readSkill,
writeSkill,
deleteSkill,
};
@@ -0,0 +1,29 @@
---
name: Audit
description: "Run a structured audit of graph quality, skill quality, and operational consistency."
when_to_use: "User asks for review, QA, cleanup, or governance checks."
when_not_to_use: "Simple one-off write/read requests."
success_criteria: "Findings are prioritized, concrete, and tied to actionable fixes."
---
# Audit
## Scope
1. Node quality: duplicates, vague descriptions, missing dates, weak titles.
2. Edge quality: missing links, weak explanations, wrong directionality.
3. Dimension quality: drift, overlap, low-signal categories.
4. Skill quality: trigger clarity, overlap, dead/unused skills.
## Output Format
1. Critical issues
2. High-impact improvements
3. Cleanup actions
4. Optional refinements
## Rules
- Prefer specific evidence over generic commentary.
- Propose the smallest high-leverage fixes first.
- Separate defects from optional polish.
@@ -0,0 +1,33 @@
---
name: Calibration
description: "Run periodic check-ins to recalibrate goals, projects, preferences, and graph structure."
when_to_use: "User asks for a check-in, reset, review, or strategic recalibration."
when_not_to_use: "Single isolated question with no strategic update needed."
success_criteria: "Graph reflects current reality: updated hubs, changed priorities, and explicit deltas."
---
# Calibration
## Objective
Re-anchor the graph to the user's current state.
## Check-in Sequence
1. Review major hub nodes and active project nodes.
2. Ask what changed: goals, priorities, constraints, beliefs, preferences.
3. Identify stale nodes and missing nodes.
4. Propose precise updates: update existing vs create new.
5. Apply changes with explicit rationale.
## Write Guidance
- Prefer updating existing nodes when continuity matters.
- Create new nodes for genuine shifts in direction/identity/approach.
- Add edges that explain evolution (old -> new).
## Output
- What changed
- What was updated vs newly created
- What should be reviewed next check-in
@@ -0,0 +1,26 @@
---
name: Connect
description: "Create and refine meaningful relationships between nodes, including integration-style synthesis links."
when_to_use: "User asks to connect ideas, merge insights, or strengthen graph relationships."
when_not_to_use: "Task is purely node creation with no relationship work required."
success_criteria: "High-signal edges improve retrieval quality and graph coherence."
---
# Connect
## Goal
Turn isolated nodes into a useful reasoning graph.
## Process
1. Identify candidate nodes to connect.
2. Check existing relationships first.
3. Add/upgrade edges with explicit directional explanation.
4. When integrating multiple nodes, create synthesis links that preserve provenance.
## Quality Bar
- Edge explanation must answer: "why does this relationship exist?"
- Direction should be intentional and interpretable.
- Avoid obvious/tautological edges.
@@ -0,0 +1,42 @@
---
name: Create Skill
description: "Design or refine a skill using a tight trigger, explicit contract, and measurable outcomes."
when_to_use: "User asks to create, rewrite, or improve a skill."
when_not_to_use: "Task is normal graph operation and no new skill is needed."
success_criteria: "Skill has clear trigger boundaries, execution steps, guardrails, and evaluation hooks."
---
# Create Skill
## Objective
Create skills that are precise, callable, and testable.
## Skill Design Standard
1. Define trigger boundary clearly: when to use and when not to use.
2. Define required outputs and quality bar.
3. Specify concrete execution sequence.
4. Add hard guardrails (what to reject/avoid).
5. Keep it short; remove fluff and duplicate policy text.
## Required Structure
- `name`
- `description`
- `when_to_use`
- `when_not_to_use`
- `success_criteria`
- Step-by-step procedure
- Do-not list
## Validation Checklist
- Can another agent execute this without guessing?
- Does it avoid overlap with existing skills?
- Are failure modes explicit?
- Is there an obvious way to evaluate success?
## Consolidation Rule
If two skills have the same trigger + same tool path + same output contract, merge them.
@@ -0,0 +1,37 @@
---
name: DB Operations
description: "Use this for all graph read/write operations with strict data quality standards."
when_to_use: "Any request to read, create, update, connect, classify, or traverse graph data."
when_not_to_use: "Pure conversation with no graph interaction needed."
success_criteria: "Writes are explicit and correct; descriptions are concrete; edges and dimensions are high-signal."
---
# DB Operations
## Core Rules
1. Search before create to avoid duplicates.
2. Every create/update must include an explicit description of WHAT the thing is and WHY it matters.
3. Use event dates when known (when it happened, not when saved).
4. Apply dimensions deliberately; prefer existing dimensions over creating noisy new ones.
5. Create edges when relationships are meaningful; edge explanations should read as a sentence.
## Write Quality Contract
- `title`: clear and specific.
- `description`: concrete object-level description, not vague summaries.
- `notes/content`: extra context, analysis, supporting detail.
- `link`: external source URL only.
## Execution Pattern
1. Read context (search + relevant nodes + relevant edges).
2. Decide: create vs update vs connect.
3. Execute minimum required writes.
4. Verify result reflects user intent exactly.
## Do Not
- Create duplicate nodes when an update is correct.
- Write vague descriptions ("discusses", "explores", "is about").
- Create weak or directionless edges.
@@ -0,0 +1,34 @@
---
name: Onboarding
description: "Onboard a new user by mapping goals, projects, worldview, and preferences into a strong initial graph."
when_to_use: "New user setup or major reset of account context."
when_not_to_use: "User asks for a narrow tactical operation only."
success_criteria: "User has an initial hub-node structure and clear next steps for graph growth."
---
# Onboarding
## Goal
Understand the user deeply enough to bootstrap a useful externalized context graph.
## Interview Flow
1. Clarify outcomes: goals for this system and success horizon.
2. Map active projects: responsibilities, timelines, decision pressure.
3. Capture worldview: beliefs, principles, working assumptions, decision criteria.
4. Capture identity/context anchors: roles, domains, recurring themes.
5. Capture interaction preferences: style, rigor, speed vs depth, tone.
## Graph Bootstrap
1. Propose 3-6 hub nodes with clear rationale.
2. Propose starter dimensions that reflect real domains.
3. Create initial edges between hubs and active projects.
4. Confirm with user before writing.
## Output
- Initial hub map
- Suggested first write actions
- Suggested weekly maintenance rhythm
@@ -0,0 +1,30 @@
---
name: Persona
description: "Build and maintain a user-defined agent persona and interaction style profile."
when_to_use: "User asks to set or refine how the agent should behave."
when_not_to_use: "No behavior/persona request is present."
success_criteria: "Persona is explicit, editable, and consistently applied across interactions."
---
# Persona
## Objective
Make agent behavior fully malleable to the user.
## Capture Model
1. Communication style (directness, brevity, tone).
2. Thinking style (exploratory vs decisive, framework-heavy vs practical).
3. Decision style (challenge level, risk posture, evidence threshold).
4. Collaboration style (pushback expectations, cadence, format preferences).
## Persistence Pattern
- Store persona as explicit nodes (and updates over time), not hidden assumptions.
- Keep versioned changes visible to the user.
## Do Not
- Freeze persona permanently.
- Apply unstated style assumptions when user instructions conflict.
@@ -0,0 +1,28 @@
---
name: Traverse
description: "Gather deeper context by traversing connected nodes before answering."
when_to_use: "Question benefits from broader context than a single direct lookup."
when_not_to_use: "Simple factual answer can be resolved from one direct node."
success_criteria: "Answer is grounded in relevant connected context without over-traversal noise."
---
# Traverse
## Strategy
1. Start with seed nodes from the user query.
2. Expand neighbors breadth-first with depth limits.
3. Prioritize high-signal nodes (strong relevance + strong edge semantics).
4. Pull key evidence from top nodes.
5. Answer directly and explain why chosen nodes mattered.
## Defaults
- Depth: 2
- Seed count: 3-5
- Exploration budget: ~40 nodes
## Do Not
- Traverse without limits.
- Include distant low-signal nodes just to add volume.