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 { NextRequest, NextResponse } from 'next/server';
|
||||||
import { readSkill, deleteSkill } from '@/services/skills/skillService';
|
import { readSkill, writeSkill, deleteSkill } from '@/services/skills/skillService';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
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
|
// Server info
|
||||||
const serverInfo = {
|
const serverInfo = {
|
||||||
name: 'ra-h-standalone',
|
name: 'ra-h-standalone',
|
||||||
version: '1.7.0'
|
version: '1.8.0'
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildInstructions() {
|
function buildInstructions() {
|
||||||
@@ -1012,7 +1012,7 @@ async function main() {
|
|||||||
'sqliteQuery',
|
'sqliteQuery',
|
||||||
{
|
{
|
||||||
title: 'Execute read-only SQL',
|
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
|
inputSchema: sqliteQueryInputSchema
|
||||||
},
|
},
|
||||||
async ({ sql: userSql, format = 'json' }) => {
|
async ({ sql: userSql, format = 'json' }) => {
|
||||||
|
|||||||
@@ -3,19 +3,19 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Plus, Pencil, Trash2, Save, X, FileText } from 'lucide-react';
|
import { Plus, Pencil, Trash2, Save, X, FileText } from 'lucide-react';
|
||||||
|
|
||||||
interface GuideMeta {
|
interface SkillMeta {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Guide extends GuideMeta {
|
interface Skill extends SkillMeta {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GuidesViewer() {
|
export default function GuidesViewer() {
|
||||||
const [guides, setGuides] = useState<GuideMeta[]>([]);
|
const [guides, setGuides] = useState<SkillMeta[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
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 [isNew, setIsNew] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -25,13 +25,13 @@ export default function GuidesViewer() {
|
|||||||
|
|
||||||
const fetchGuides = async () => {
|
const fetchGuides = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/guides');
|
const res = await fetch('/api/skills');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setGuides(data.data);
|
setGuides(data.data);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch guides:', err);
|
console.error('Failed to fetch skills:', err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ export default function GuidesViewer() {
|
|||||||
|
|
||||||
const handleEdit = async (name: string) => {
|
const handleEdit = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/guides/${encodeURIComponent(name)}`);
|
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
setEditing(data.data);
|
setEditing(data.data);
|
||||||
@@ -47,16 +47,16 @@ export default function GuidesViewer() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch guide:', err);
|
console.error('Failed to fetch skill:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNew = () => {
|
const handleNew = () => {
|
||||||
setEditing({
|
setEditing({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
content: '# New Guide\n\nWrite your guide content here...',
|
content: '# New Skill\n\nWrite your skill content here...',
|
||||||
});
|
});
|
||||||
setIsNew(true);
|
setIsNew(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
@@ -69,7 +69,7 @@ export default function GuidesViewer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/guides/${encodeURIComponent(editing.name)}`, {
|
const res = await fetch(`/api/skills/${encodeURIComponent(editing.name)}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -81,20 +81,21 @@ export default function GuidesViewer() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
fetchGuides();
|
fetchGuides();
|
||||||
|
window.dispatchEvent(new Event('skills:updated'));
|
||||||
window.dispatchEvent(new Event('guides:updated'));
|
window.dispatchEvent(new Event('guides:updated'));
|
||||||
} else {
|
} else {
|
||||||
setError(data.error || 'Failed to save');
|
setError(data.error || 'Failed to save');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Failed to save guide');
|
setError('Failed to save skill');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (name: string) => {
|
const handleDelete = async (name: string) => {
|
||||||
if (!confirm(`Delete guide "${name}"?`)) return;
|
if (!confirm(`Delete skill "${name}"?`)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/guides/${encodeURIComponent(name)}`, {
|
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -103,13 +104,13 @@ export default function GuidesViewer() {
|
|||||||
window.dispatchEvent(new Event('guides:updated'));
|
window.dispatchEvent(new Event('guides:updated'));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to delete guide:', err);
|
console.error('Failed to delete skill:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
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"
|
type="text"
|
||||||
value={editing.name}
|
value={editing.name}
|
||||||
onChange={(e) => setEditing({ ...editing, name: e.target.value })}
|
onChange={(e) => setEditing({ ...editing, name: e.target.value })}
|
||||||
placeholder="Guide name"
|
placeholder="Skill name"
|
||||||
disabled={!isNew}
|
disabled={!isNew}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -193,7 +194,7 @@ export default function GuidesViewer() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={editing.content}
|
value={editing.content}
|
||||||
onChange={(e) => setEditing({ ...editing, content: e.target.value })}
|
onChange={(e) => setEditing({ ...editing, content: e.target.value })}
|
||||||
placeholder="Guide content (markdown)"
|
placeholder="Skill content (markdown)"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: '12px',
|
padding: '12px',
|
||||||
@@ -209,7 +210,7 @@ export default function GuidesViewer() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<p style={{ color: '#666', fontSize: '12px', marginTop: '12px' }}>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -219,7 +220,7 @@ export default function GuidesViewer() {
|
|||||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
|
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||||
<p style={{ color: '#888', fontSize: '13px', margin: 0 }}>
|
<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>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleNew}
|
onClick={handleNew}
|
||||||
@@ -237,7 +238,7 @@ export default function GuidesViewer() {
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus size={14} /> New Guide
|
<Plus size={14} /> New Skill
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -245,8 +246,8 @@ export default function GuidesViewer() {
|
|||||||
{guides.length === 0 ? (
|
{guides.length === 0 ? (
|
||||||
<div style={{ color: '#555', textAlign: 'center', paddingTop: '48px' }}>
|
<div style={{ color: '#555', textAlign: 'center', paddingTop: '48px' }}>
|
||||||
<FileText size={48} style={{ marginBottom: '12px', opacity: 0.5 }} />
|
<FileText size={48} style={{ marginBottom: '12px', opacity: 0.5 }} />
|
||||||
<p style={{ fontSize: '14px' }}>No guides yet</p>
|
<p style={{ fontSize: '14px' }}>No skills yet</p>
|
||||||
<p style={{ fontSize: '12px', color: '#444' }}>Create guides to help external agents understand your knowledge base</p>
|
<p style={{ fontSize: '12px', color: '#444' }}>Create skills to guide how agents operate in your knowledge base</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ApiKeysViewer from './ApiKeysViewer';
|
|||||||
import DatabaseViewer from './DatabaseViewer';
|
import DatabaseViewer from './DatabaseViewer';
|
||||||
import ExternalAgentsPanel from './ExternalAgentsPanel';
|
import ExternalAgentsPanel from './ExternalAgentsPanel';
|
||||||
import ContextViewer from './ContextViewer';
|
import ContextViewer from './ContextViewer';
|
||||||
import GuidesViewer from './GuidesViewer';
|
import SkillsViewer from './GuidesViewer';
|
||||||
export type SettingsTab =
|
export type SettingsTab =
|
||||||
| 'logs'
|
| 'logs'
|
||||||
| 'tools'
|
| 'tools'
|
||||||
@@ -140,7 +140,7 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
|
|||||||
transition: 'all 0.2s'
|
transition: 'all 0.2s'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Guides
|
Skills
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => setActiveTab('apikeys')}
|
onClick={() => setActiveTab('apikeys')}
|
||||||
@@ -284,7 +284,7 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
|
|||||||
>
|
>
|
||||||
{activeTab === 'logs' && 'System Logs'}
|
{activeTab === 'logs' && 'System Logs'}
|
||||||
{activeTab === 'tools' && 'Tools'}
|
{activeTab === 'tools' && 'Tools'}
|
||||||
{activeTab === 'guides' && 'Guides'}
|
{activeTab === 'guides' && 'Skills'}
|
||||||
{activeTab === 'apikeys' && 'API Keys'}
|
{activeTab === 'apikeys' && 'API Keys'}
|
||||||
{activeTab === 'database' && 'Knowledge Database'}
|
{activeTab === 'database' && 'Knowledge Database'}
|
||||||
{activeTab === 'context' && 'Auto-Context'}
|
{activeTab === 'context' && 'Auto-Context'}
|
||||||
@@ -318,7 +318,7 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
|
|||||||
<div style={{ flex: 1, overflow: 'hidden' }}>
|
<div style={{ flex: 1, overflow: 'hidden' }}>
|
||||||
{activeTab === 'logs' && <LogsViewer key={isOpen ? 'open' : 'closed'} />}
|
{activeTab === 'logs' && <LogsViewer key={isOpen ? 'open' : 'closed'} />}
|
||||||
{activeTab === 'tools' && <ToolsViewer />}
|
{activeTab === 'tools' && <ToolsViewer />}
|
||||||
{activeTab === 'guides' && <GuidesViewer />}
|
{activeTab === 'guides' && <SkillsViewer />}
|
||||||
{activeTab === 'apikeys' && <ApiKeysViewer />}
|
{activeTab === 'apikeys' && <ApiKeysViewer />}
|
||||||
{activeTab === 'database' && <DatabaseViewer />}
|
{activeTab === 'database' && <DatabaseViewer />}
|
||||||
{activeTab === 'context' && <ContextViewer />}
|
{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(),
|
process.cwd(),
|
||||||
'src/config/skills'
|
'src/config/skills'
|
||||||
);
|
);
|
||||||
|
const SEED_MIGRATION_FLAG = path.join(SKILLS_DIR, '.seed-migrated-2026-03-07-skills-overhaul');
|
||||||
|
|
||||||
const SEEDED_SKILL_IDS = new Set([
|
const SEEDED_SKILL_IDS = new Set([
|
||||||
'db-operations',
|
'db-operations',
|
||||||
@@ -39,6 +40,25 @@ const SEEDED_SKILL_IDS = new Set([
|
|||||||
'connect',
|
'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 {
|
function ensureSkillsDir(): void {
|
||||||
if (!fs.existsSync(SKILLS_DIR)) {
|
if (!fs.existsSync(SKILLS_DIR)) {
|
||||||
fs.mkdirSync(SKILLS_DIR, { recursive: true });
|
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 {
|
function resolveSkillFilename(name: string): string | null {
|
||||||
const files = listMarkdownFiles(SKILLS_DIR);
|
const files = listMarkdownFiles(SKILLS_DIR);
|
||||||
const normalizedInput = normalizeSkillId(name);
|
const normalizedInput = normalizeSkillId(name);
|
||||||
@@ -130,7 +181,9 @@ function init(): void {
|
|||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
ensureSkillsDir();
|
ensureSkillsDir();
|
||||||
migrateLegacyGuides();
|
migrateLegacyGuides();
|
||||||
|
migrateSeededBaseline();
|
||||||
seedSkills();
|
seedSkills();
|
||||||
|
pruneDeprecatedSkills();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user