cleanup(skills): remove legacy guide bundle and harden skill migration/pruning
This commit is contained in:
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' }) => {
|
||||
|
||||
@@ -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,7 +47,7 @@ export default function GuidesViewer() {
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch guide:', err);
|
||||
console.error('Failed to fetch skill:', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function GuidesViewer() {
|
||||
setEditing({
|
||||
name: '',
|
||||
description: '',
|
||||
content: '# New Guide\n\nWrite your guide content here...',
|
||||
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' }}>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -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.**
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user