cleanup(skills): remove legacy guide bundle and harden skill migration/pruning

This commit is contained in:
“BeeRad”
2026-03-07 12:00:52 +11:00
parent b67a789740
commit bc226b03c8
16 changed files with 134 additions and 581 deletions
+48 -1
View File
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { readSkill, deleteSkill } from '@/services/skills/skillService';
import { readSkill, writeSkill, deleteSkill } from '@/services/skills/skillService';
export const runtime = 'nodejs';
@@ -50,3 +50,50 @@ export async function DELETE(
);
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ name: string }> }
) {
try {
const { name } = await params;
const body = await request.json();
const description = typeof body?.description === 'string' ? body.description.trim() : '';
const content = typeof body?.content === 'string' ? body.content : '';
const safeName = name.trim();
if (!safeName) {
return NextResponse.json(
{ success: false, error: 'Skill name is required' },
{ status: 400 }
);
}
const safeDescription = description.replace(/\r?\n/g, ' ').trim();
const frontmatter = [
'---',
`name: ${safeName}`,
`description: ${safeDescription}`,
'---',
'',
content.trim(),
'',
].join('\n');
const result = writeSkill(safeName, frontmatter);
if (!result.success) {
return NextResponse.json(
{ success: false, error: result.error },
{ status: 400 }
);
}
return NextResponse.json({ success: true, message: `Skill "${safeName}" saved` });
} catch (error) {
console.error('[API /skills/[name] PUT] error:', error);
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Failed to save skill' },
{ status: 500 }
);
}
}
+2 -2
View File
@@ -37,7 +37,7 @@ const skillService = require('./services/skillService');
// Server info
const serverInfo = {
name: 'ra-h-standalone',
version: '1.7.0'
version: '1.8.0'
};
function buildInstructions() {
@@ -1012,7 +1012,7 @@ async function main() {
'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. Use when structured tools are insufficient — e.g., complex JOINs, aggregations, or custom filtering. Read readSkill("schema") for table definitions and query patterns.',
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("db-operations") for table definitions and query patterns.',
inputSchema: sqliteQueryInputSchema
},
async ({ sql: userSql, format = 'json' }) => {
+27 -26
View File
@@ -3,19 +3,19 @@
import { useState, useEffect } from 'react';
import { Plus, Pencil, Trash2, Save, X, FileText } from 'lucide-react';
interface GuideMeta {
interface SkillMeta {
name: string;
description: string;
}
interface Guide extends GuideMeta {
interface Skill extends SkillMeta {
content: string;
}
export default function GuidesViewer() {
const [guides, setGuides] = useState<GuideMeta[]>([]);
const [guides, setGuides] = useState<SkillMeta[]>([]);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState<Guide | null>(null);
const [editing, setEditing] = useState<Skill | null>(null);
const [isNew, setIsNew] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -25,13 +25,13 @@ export default function GuidesViewer() {
const fetchGuides = async () => {
try {
const res = await fetch('/api/guides');
const res = await fetch('/api/skills');
const data = await res.json();
if (data.success) {
setGuides(data.data);
}
} catch (err) {
console.error('Failed to fetch guides:', err);
console.error('Failed to fetch skills:', err);
} finally {
setLoading(false);
}
@@ -39,7 +39,7 @@ export default function GuidesViewer() {
const handleEdit = async (name: string) => {
try {
const res = await fetch(`/api/guides/${encodeURIComponent(name)}`);
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`);
const data = await res.json();
if (data.success) {
setEditing(data.data);
@@ -47,16 +47,16 @@ export default function GuidesViewer() {
setError(null);
}
} catch (err) {
console.error('Failed to fetch guide:', err);
console.error('Failed to fetch skill:', err);
}
};
const handleNew = () => {
setEditing({
name: '',
description: '',
content: '# New Guide\n\nWrite your guide content here...',
});
setEditing({
name: '',
description: '',
content: '# New Skill\n\nWrite your skill content here...',
});
setIsNew(true);
setError(null);
};
@@ -69,7 +69,7 @@ export default function GuidesViewer() {
}
try {
const res = await fetch(`/api/guides/${encodeURIComponent(editing.name)}`, {
const res = await fetch(`/api/skills/${encodeURIComponent(editing.name)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -81,20 +81,21 @@ export default function GuidesViewer() {
if (data.success) {
setEditing(null);
fetchGuides();
window.dispatchEvent(new Event('skills:updated'));
window.dispatchEvent(new Event('guides:updated'));
} else {
setError(data.error || 'Failed to save');
}
} catch (err) {
setError('Failed to save guide');
setError('Failed to save skill');
}
};
const handleDelete = async (name: string) => {
if (!confirm(`Delete guide "${name}"?`)) return;
if (!confirm(`Delete skill "${name}"?`)) return;
try {
const res = await fetch(`/api/guides/${encodeURIComponent(name)}`, {
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, {
method: 'DELETE',
});
const data = await res.json();
@@ -103,13 +104,13 @@ export default function GuidesViewer() {
window.dispatchEvent(new Event('guides:updated'));
}
} catch (err) {
console.error('Failed to delete guide:', err);
console.error('Failed to delete skill:', err);
}
};
if (loading) {
return (
<div style={{ padding: '24px', color: '#666' }}>Loading guides...</div>
<div style={{ padding: '24px', color: '#666' }}>Loading skills...</div>
);
}
@@ -121,7 +122,7 @@ export default function GuidesViewer() {
type="text"
value={editing.name}
onChange={(e) => setEditing({ ...editing, name: e.target.value })}
placeholder="Guide name"
placeholder="Skill name"
disabled={!isNew}
style={{
flex: 1,
@@ -193,7 +194,7 @@ export default function GuidesViewer() {
<textarea
value={editing.content}
onChange={(e) => setEditing({ ...editing, content: e.target.value })}
placeholder="Guide content (markdown)"
placeholder="Skill content (markdown)"
style={{
flex: 1,
padding: '12px',
@@ -209,7 +210,7 @@ export default function GuidesViewer() {
/>
<p style={{ color: '#666', fontSize: '12px', marginTop: '12px' }}>
Guides are markdown files that external agents can read via MCP tools. Use them to provide context, instructions, or reference material.
Skills are markdown files that agents can read for reusable procedural instructions.
</p>
</div>
);
@@ -219,7 +220,7 @@ export default function GuidesViewer() {
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<p style={{ color: '#888', fontSize: '13px', margin: 0 }}>
Guides provide context and instructions for external AI agents via MCP.
Skills provide reusable context and procedural instructions for agents.
</p>
<button
onClick={handleNew}
@@ -237,7 +238,7 @@ export default function GuidesViewer() {
fontWeight: 500,
}}
>
<Plus size={14} /> New Guide
<Plus size={14} /> New Skill
</button>
</div>
@@ -245,8 +246,8 @@ export default function GuidesViewer() {
{guides.length === 0 ? (
<div style={{ color: '#555', textAlign: 'center', paddingTop: '48px' }}>
<FileText size={48} style={{ marginBottom: '12px', opacity: 0.5 }} />
<p style={{ fontSize: '14px' }}>No guides yet</p>
<p style={{ fontSize: '12px', color: '#444' }}>Create guides to help external agents understand your knowledge base</p>
<p style={{ fontSize: '14px' }}>No skills yet</p>
<p style={{ fontSize: '12px', color: '#444' }}>Create skills to guide how agents operate in your knowledge base</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
+4 -4
View File
@@ -8,7 +8,7 @@ import ApiKeysViewer from './ApiKeysViewer';
import DatabaseViewer from './DatabaseViewer';
import ExternalAgentsPanel from './ExternalAgentsPanel';
import ContextViewer from './ContextViewer';
import GuidesViewer from './GuidesViewer';
import SkillsViewer from './GuidesViewer';
export type SettingsTab =
| 'logs'
| 'tools'
@@ -140,7 +140,7 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
transition: 'all 0.2s'
}}
>
Guides
Skills
</div>
<div
onClick={() => setActiveTab('apikeys')}
@@ -284,7 +284,7 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
>
{activeTab === 'logs' && 'System Logs'}
{activeTab === 'tools' && 'Tools'}
{activeTab === 'guides' && 'Guides'}
{activeTab === 'guides' && 'Skills'}
{activeTab === 'apikeys' && 'API Keys'}
{activeTab === 'database' && 'Knowledge Database'}
{activeTab === 'context' && 'Auto-Context'}
@@ -318,7 +318,7 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
<div style={{ flex: 1, overflow: 'hidden' }}>
{activeTab === 'logs' && <LogsViewer key={isOpen ? 'open' : 'closed'} />}
{activeTab === 'tools' && <ToolsViewer />}
{activeTab === 'guides' && <GuidesViewer />}
{activeTab === 'guides' && <SkillsViewer />}
{activeTab === 'apikeys' && <ApiKeysViewer />}
{activeTab === 'database' && <DatabaseViewer />}
{activeTab === 'context' && <ContextViewer />}
-42
View File
@@ -1,42 +0,0 @@
---
name: Connect
description: Quick link — find explicitly related nodes and create edges between them.
---
# Connect
Quick link: find explicitly related nodes and create edges.
## Steps
1. **Read Node**
Call `getNodesById` for the focused node. Extract the main topic/subject from the title.
2. **Quick Search**
Call `queryNodes` with the main topic from the node title.
- `search`: the key term from the title (e.g., if title mentions "Nietzsche", search "Nietzsche")
- `limit`: 10
- Do NOT add dimensions filter — search across all nodes
3. **Create Edges**
From results, pick 2-4 clearly related nodes.
Call `createEdge` for each:
- `from_node_id`: focused node ID
- `to_node_id`: related node ID
- `explanation`: "brief reason for this connection"
Direction rule: write the explanation so it reads FROM → TO.
Examples:
- Episode → Podcast: "Episode of this podcast"
- Book → Author: "Written by"
4. **Done**
Reply: "Linked [NODE:id:title] → [list of connected nodes as NODE:id:title]"
## Rules
- Total tool calls ≤ 5
- Search the MAIN TOPIC from the title, not random names from content
- NO dimensions filter in `queryNodes` — search everything
- Only link nodes with clear relationships
- Skip if no matches found
-71
View File
@@ -1,71 +0,0 @@
---
name: Integrate
description: Full analysis — find connections across the knowledge graph, create edges, and document an integration analysis.
---
# Integrate
Find meaningful connections across the knowledge graph, create edges, and append an Integration Analysis.
## Steps
1. **Retrieve & Understand**
- Call `getNodesById` for the focused node
- Identify: what type of thing is this? (person, project, paper, idea, video, etc.)
- Extract key entities: names, projects, concepts, techniques
- Note the core insight in one sentence
2. **Search for Connections**
Search the database using entities from step 1:
a) Structural connections:
- Names mentioned → `queryNodes` to find nodes about those people
- Projects/tools mentioned → `queryNodes` to find those nodes
b) Thematic connections:
- Use `searchContentEmbeddings` with key concepts
- Look for shared themes, complementary ideas, contradictions
Target: 3-5 strong connections (quality over quantity)
3. **Create Edges**
For each connection found, call `createEdge`:
- `from_node_id`: the focused node ID
- `to_node_id`: the connected node ID
- `explanation`: "why this connection matters"
Direction rule: write the explanation so it reads FROM → TO.
Examples:
- Insight → Source: "Came from / inspired by"
- Episode → Podcast: "Episode of this podcast"
The tool handles duplicates gracefully — if edge exists, it returns an error and you continue.
Create 3-5 edges total.
4. **Document in Content**
Call `updateNode` ONCE using the `content` field with ONLY this new section:
```
---
## Integration Analysis
[2-3 sentences: what this is, why it matters, core insight]
**Connections:**
- [NODE:123:"Title"] — [why connected]
- [NODE:456:"Title"] — [why connected]
```
Use `updates.content` (NOT chunk). Send ONLY the new section. The tool appends automatically.
5. **Return Summary**
Reply with: Task / Actions / Result / Nodes / Follow-up (≤100 words)
## Rules
- Keep total tool calls ≤ 12
- Create edges BEFORE documenting (step 3 before step 4)
- Call `updateNode` exactly once
- Adapt to any node type
- Use `think` at any point if you need to plan your approach
-14
View File
@@ -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
-39
View File
@@ -1,39 +0,0 @@
---
name: Prep
description: Quick summary brief — extract the gist to help decide if content is worth deeper engagement.
---
# Prep
Quick summary to help the user decide if content is worth deeper engagement.
## Steps
1. **Read the Node**
- Call `getNodesById` for the focused node
- Understand what this is and extract the core message
2. **Append Brief**
Call `updateNode` ONCE using the `content` field with ONLY this section:
```
---
## Brief
**What:** [One sentence — what is this?]
**Gist:** [2-3 sentences — the core message or takeaway]
**Why it matters:** [1-2 sentences — relevance or implications]
```
Use `updates.content` (NOT chunk). Send ONLY the new section. The tool appends automatically.
3. **Return Summary**
Reply with a one-line confirmation: "Prepped [title] — [gist in <10 words]"
## Rules
- Keep total tool calls ≤ 3
- Call `updateNode` exactly once
- Be concise — this is a quick prep, not deep analysis
-48
View File
@@ -1,48 +0,0 @@
---
name: Research
description: Deep research — conduct background web research on a topic and append findings to the node.
---
# Research
Conduct background research on the topic/person/concept and append findings to the node.
## Steps
1. **Read & Identify**
- Call `getNodesById` for the focused node
- Identify: what needs researching? (person's background, concept origins, recent developments, etc.)
2. **Web Research**
- Call `webSearch` with targeted queries (1-2 searches)
- Focus on: background context, recent news, authoritative sources
- Extract the most relevant findings
3. **Append Research**
Call `updateNode` ONCE using the `content` field with ONLY this section:
```
---
## Research Notes
**Background:** [2-3 sentences of context]
**Key Findings:**
- [Finding 1]
- [Finding 2]
- [Finding 3]
**Sources:** [Brief attribution]
```
Use `updates.content` (NOT chunk). Send ONLY the new section. The tool appends automatically.
4. **Return Summary**
Reply with: "Researched [topic] — [key insight in <15 words]"
## Rules
- Keep total tool calls ≤ 5
- Call `updateNode` exactly once
- Focus on factual background, not opinion
- Cite sources when possible
-53
View File
@@ -1,53 +0,0 @@
---
name: Survey
description: Survey a dimension — analyze patterns, themes, gaps, and insights across all nodes within it.
---
# Survey
Analyze the active dimension to identify patterns, themes, gaps, and insights across all nodes within it.
**Prerequisite:** This guide requires an active dimension. Check the ACTIVE DIMENSION section in context.
## Steps
1. **Retrieve Dimension Nodes**
- Call `queryDimensionNodes` with the active dimension name
- Set limit to 50 to get a comprehensive view
- Note the total count and most connected nodes
2. **Analyze Themes**
- Call `searchContentEmbeddings` with key concepts from the dimension
- Identify recurring themes, patterns, and relationships
- Note any gaps or underrepresented areas
3. **Document Findings**
Call `updateDimension` to update the dimension description with:
```
**Survey Summary**
**Node Count:** [X nodes]
**Top Hubs:** [List 3-5 most connected nodes with [NODE:id:"title"]]
**Themes:**
- [Theme 1 — brief description]
- [Theme 2 — brief description]
**Gaps/Opportunities:**
- [Identified gap or area for expansion]
**Connections to Other Dimensions:**
- [Any cross-dimension patterns observed]
```
4. **Return Summary**
Reply with: "Surveyed [dimension] — [X nodes, key insight in <15 words]"
## Rules
- Keep total tool calls ≤ 5
- Focus on patterns across the dimension, not individual node details
- Identify both strengths (dense areas) and gaps (sparse areas)
- Reference specific nodes using `[NODE:id:"title"]` format
- If no active dimension is set, inform the user to open a dimension folder first
@@ -1,48 +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 priority dimensions — assign these when relevant
- If no existing dimension fits, create a new one (but prefer existing)
## Description Field
The description is the most important field for AI context. It powers search (5x boost),
appears in system prompts, and determines how agents understand the node.
**Standard:** State WHAT this is + WHY it matters. Extremely concise, high-level. Max 280 characters.
**Rules:**
- NO weak verbs: "discusses", "explores", "examines", "talks about"
- State the actual claim, insight, or purpose directly
- Include significance or implication in 1 phrase
- If auto-generated (omitted), the system will generate one — but agent-written descriptions are always better
**Good:** "By Karpathy — Software is becoming fluid: agents can rip functionality from repos instead of taking dependencies."
**Bad:** "This article discusses the importance of software becoming more fluid."
-38
View File
@@ -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
-51
View File
@@ -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
-33
View File
@@ -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
-111
View File
@@ -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.**
+53
View File
@@ -27,6 +27,7 @@ const BUNDLED_SKILLS_DIR = path.join(
process.cwd(),
'src/config/skills'
);
const SEED_MIGRATION_FLAG = path.join(SKILLS_DIR, '.seed-migrated-2026-03-07-skills-overhaul');
const SEEDED_SKILL_IDS = new Set([
'db-operations',
@@ -39,6 +40,25 @@ const SEEDED_SKILL_IDS = new Set([
'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 ensureSkillsDir(): void {
if (!fs.existsSync(SKILLS_DIR)) {
fs.mkdirSync(SKILLS_DIR, { recursive: true });
@@ -93,6 +113,37 @@ function seedSkills(): void {
}
}
function migrateSeededBaseline(): void {
if (fs.existsSync(SEED_MIGRATION_FLAG)) {
return;
}
const bundledFiles = listMarkdownFiles(BUNDLED_SKILLS_DIR);
const bundledById = new Map<string, string>();
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(): void {
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: string): string | null {
const files = listMarkdownFiles(SKILLS_DIR);
const normalizedInput = normalizeSkillId(name);
@@ -130,7 +181,9 @@ function init(): void {
if (initialized) return;
ensureSkillsDir();
migrateLegacyGuides();
migrateSeededBaseline();
seedSkills();
pruneDeprecatedSkills();
initialized = true;
}