feat: sync shared skills contract and graph-first discipline
This commit is contained in:
@@ -702,14 +702,6 @@ export default function ThreePanelLayout() {
|
||||
case 'SKILL_UPDATED':
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('skills:updated'));
|
||||
window.dispatchEvent(new CustomEvent('guides:updated'));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'GUIDE_UPDATED':
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('skills:updated'));
|
||||
window.dispatchEvent(new CustomEvent('guides:updated'));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,330 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ArrowLeft, Lock, Trash2 } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import PaneHeader from './PaneHeader';
|
||||
import type { BasePaneProps } from './types';
|
||||
|
||||
interface GuideMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
immutable: boolean;
|
||||
}
|
||||
|
||||
interface Guide extends GuideMeta {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default function GuidesPane({
|
||||
slot,
|
||||
isActive,
|
||||
onPaneAction,
|
||||
onCollapse,
|
||||
onSwapPanes,
|
||||
tabBar,
|
||||
}: BasePaneProps) {
|
||||
const [guides, setGuides] = useState<GuideMeta[]>([]);
|
||||
const [selectedGuide, setSelectedGuide] = useState<Guide | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGuides();
|
||||
|
||||
const handleGuideUpdated = () => { fetchGuides(); };
|
||||
window.addEventListener('guides:updated', handleGuideUpdated);
|
||||
return () => window.removeEventListener('guides:updated', handleGuideUpdated);
|
||||
}, []);
|
||||
|
||||
const fetchGuides = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/guides');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setGuides(data.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[GuidesPane] Failed to fetch guides:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectGuide = async (name: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/guides/${encodeURIComponent(name)}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setSelectedGuide(data.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[GuidesPane] Failed to fetch guide:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteGuide = async (name: string, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm(`Delete guide "${name}"?`)) return;
|
||||
|
||||
setDeleting(name);
|
||||
try {
|
||||
const res = await fetch(`/api/guides/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
fetchGuides();
|
||||
if (selectedGuide?.name === name) {
|
||||
setSelectedGuide(null);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[GuidesPane] Failed to delete guide:', err);
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setSelectedGuide(null);
|
||||
};
|
||||
|
||||
const systemGuides = guides.filter(g => g.immutable);
|
||||
const userGuides = guides.filter(g => !g.immutable);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes} tabBar={tabBar}>
|
||||
{selectedGuide ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<button
|
||||
onClick={handleBack}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'var(--rah-text-muted)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.color = 'var(--rah-text-secondary)'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.color = 'var(--rah-text-muted)'; }}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<span style={{ color: 'var(--rah-text-secondary)', fontSize: '13px', fontWeight: 500, display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
{selectedGuide.immutable && <Lock size={12} style={{ color: '#22c55e' }} />}
|
||||
{selectedGuide.name}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: 'var(--rah-text-muted)', fontSize: '11px' }}>
|
||||
{userGuides.length} of 10 custom guides
|
||||
</span>
|
||||
)}
|
||||
</PaneHeader>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px' }}>
|
||||
{loading ? (
|
||||
<div style={{ color: 'var(--rah-text-muted)', fontSize: '13px', textAlign: 'center', paddingTop: '24px' }}>
|
||||
Loading...
|
||||
</div>
|
||||
) : selectedGuide ? (
|
||||
<div className="guide-content" style={{ color: 'var(--rah-text-secondary)', fontSize: '13px', lineHeight: '1.6' }}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 style={{ fontSize: '18px', fontWeight: 600, color: 'var(--rah-text-base)', margin: '0 0 16px 0' }}>{children}</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 style={{ fontSize: '15px', fontWeight: 600, color: 'var(--rah-text-base)', margin: '20px 0 8px 0' }}>{children}</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 style={{ fontSize: '14px', fontWeight: 600, color: 'var(--rah-text-secondary)', margin: '16px 0 6px 0' }}>{children}</h3>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p style={{ margin: '0 0 12px 0' }}>{children}</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul style={{ margin: '0 0 12px 0', paddingLeft: '20px' }}>{children}</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol style={{ margin: '0 0 12px 0', paddingLeft: '20px' }}>{children}</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li style={{ margin: '0 0 4px 0' }}>{children}</li>
|
||||
),
|
||||
code: ({ className, children, ...props }) => {
|
||||
const isInline = !className;
|
||||
if (isInline) {
|
||||
return (
|
||||
<code style={{
|
||||
background: 'var(--rah-bg-active)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: '#22c55e',
|
||||
}} {...props}>{children}</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code style={{
|
||||
display: 'block',
|
||||
background: 'var(--rah-bg-surface)',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
overflowX: 'auto',
|
||||
margin: '0 0 12px 0',
|
||||
color: 'var(--rah-text-muted)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}} {...props}>{children}</code>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre style={{ margin: '0 0 12px 0' }}>{children}</pre>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong style={{ color: 'var(--rah-text-base)', fontWeight: 600 }}>{children}</strong>
|
||||
),
|
||||
hr: () => (
|
||||
<hr style={{ border: 'none', borderTop: '1px solid var(--rah-border-strong)', margin: '16px 0' }} />
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote style={{
|
||||
borderLeft: '1px solid var(--rah-border-stronger)',
|
||||
paddingLeft: '12px',
|
||||
margin: '0 0 12px 0',
|
||||
color: 'var(--rah-text-muted)',
|
||||
}}>{children}</blockquote>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{selectedGuide.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{guides.length === 0 ? (
|
||||
<div style={{ color: 'var(--rah-text-muted)', fontSize: '13px', textAlign: 'center', paddingTop: '24px' }}>
|
||||
No guides found
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{systemGuides.length > 0 && (
|
||||
<div style={{ color: 'var(--rah-text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.5px', padding: '4px 0' }}>
|
||||
System Guides
|
||||
</div>
|
||||
)}
|
||||
{systemGuides.map((guide) => (
|
||||
<GuideCard
|
||||
key={guide.name}
|
||||
guide={guide}
|
||||
onSelect={handleSelectGuide}
|
||||
onDelete={handleDeleteGuide}
|
||||
deleting={deleting}
|
||||
/>
|
||||
))}
|
||||
{userGuides.length > 0 && (
|
||||
<div style={{ color: 'var(--rah-text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.5px', padding: '8px 0 4px 0' }}>
|
||||
Custom Guides
|
||||
</div>
|
||||
)}
|
||||
{userGuides.map((guide) => (
|
||||
<GuideCard
|
||||
key={guide.name}
|
||||
guide={guide}
|
||||
onSelect={handleSelectGuide}
|
||||
onDelete={handleDeleteGuide}
|
||||
deleting={deleting}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GuideCard({
|
||||
guide,
|
||||
onSelect,
|
||||
onDelete,
|
||||
deleting,
|
||||
}: {
|
||||
guide: GuideMeta;
|
||||
onSelect: (name: string) => void;
|
||||
onDelete: (name: string, e: React.MouseEvent) => void;
|
||||
deleting: string | null;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onSelect(guide.name)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '12px',
|
||||
background: 'var(--rah-bg-elevated)',
|
||||
border: '1px solid var(--rah-bg-active)',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.background = 'var(--rah-bg-active)';
|
||||
e.currentTarget.style.borderColor = 'var(--rah-border-stronger)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.background = 'var(--rah-bg-elevated)';
|
||||
e.currentTarget.style.borderColor = 'var(--rah-bg-active)';
|
||||
}}
|
||||
>
|
||||
{guide.immutable && (
|
||||
<Lock size={12} style={{ color: '#22c55e', flexShrink: 0 }} />
|
||||
)}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<span style={{ color: 'var(--rah-text-base)', fontSize: '13px', fontWeight: 500 }}>
|
||||
{guide.name}
|
||||
</span>
|
||||
<span style={{ color: 'var(--rah-text-muted)', fontSize: '12px', lineHeight: '1.4', display: 'block', marginTop: '2px' }}>
|
||||
{guide.description}
|
||||
</span>
|
||||
</div>
|
||||
{!guide.immutable && (
|
||||
<button
|
||||
onClick={(e) => onDelete(guide.name, e)}
|
||||
disabled={deleting === guide.name}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: 'var(--rah-text-muted)',
|
||||
cursor: 'pointer',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
opacity: deleting === guide.name ? 0.3 : 1,
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.color = '#ef4444'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.color = 'var(--rah-text-muted)'; }}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ArrowLeft, Brain } from 'lucide-react';
|
||||
import PaneHeader from './PaneHeader';
|
||||
import type { BasePaneProps } from './types';
|
||||
import type { FocusedSkill, Skill, SkillMeta } from '@/types/skills';
|
||||
import SkillCard from '@/components/skills/SkillCard';
|
||||
import SkillMarkdown from '@/components/skills/SkillMarkdown';
|
||||
import type { FocusedSkill } from '@/types/skills';
|
||||
|
||||
interface SkillMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
immutable: boolean;
|
||||
}
|
||||
|
||||
interface Skill extends SkillMeta {
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface SkillsPaneProps extends BasePaneProps {
|
||||
focusedSkill?: FocusedSkill | null;
|
||||
@@ -30,35 +20,19 @@ export default function SkillsPane({
|
||||
onCollapse,
|
||||
onSwapPanes,
|
||||
tabBar,
|
||||
focusedSkill,
|
||||
onFocusSkill,
|
||||
autoOpenSkillName,
|
||||
onAutoOpenHandled,
|
||||
}: SkillsPaneProps) {
|
||||
const [skills, setSkills] = useState<SkillMeta[]>([]);
|
||||
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
const detailScrollRef = useRef<HTMLDivElement>(null);
|
||||
const isSelectedSkillFocused = !!selectedSkill && focusedSkill?.name === selectedSkill.name;
|
||||
|
||||
useEffect(() => {
|
||||
void fetchSkills();
|
||||
|
||||
const handleSkillUpdated = () => {
|
||||
void fetchSkills();
|
||||
};
|
||||
window.addEventListener('skills:updated', handleSkillUpdated);
|
||||
window.addEventListener('guides:updated', handleSkillUpdated);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('skills:updated', handleSkillUpdated);
|
||||
window.removeEventListener('guides:updated', handleSkillUpdated);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSkill && detailScrollRef.current) {
|
||||
detailScrollRef.current.scrollTo({ top: 0, behavior: 'auto' });
|
||||
}
|
||||
}, [selectedSkill?.name]);
|
||||
|
||||
const fetchSkills = async () => {
|
||||
const fetchSkills = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/skills');
|
||||
const data = await res.json();
|
||||
@@ -70,18 +44,54 @@ export default function SkillsPane({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSelectSkill = async (name: string) => {
|
||||
const handleSelectSkill = useCallback(async (name: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setSelectedSkill(data.data);
|
||||
onFocusSkill?.(data.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SkillsPane] Failed to fetch skill:', err);
|
||||
}
|
||||
}, [onFocusSkill]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkills();
|
||||
|
||||
const handleSkillUpdated = () => {
|
||||
void fetchSkills();
|
||||
};
|
||||
window.addEventListener('skills:updated', handleSkillUpdated);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('skills:updated', handleSkillUpdated);
|
||||
};
|
||||
}, [fetchSkills]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSkill && detailScrollRef.current) {
|
||||
detailScrollRef.current.scrollTo({ top: 0, behavior: 'auto' });
|
||||
}
|
||||
}, [selectedSkill?.name]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoOpenSkillName || loading || selectedSkill) {
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
await handleSelectSkill(autoOpenSkillName);
|
||||
onAutoOpenHandled?.();
|
||||
})();
|
||||
}, [autoOpenSkillName, handleSelectSkill, loading, onAutoOpenHandled, selectedSkill]);
|
||||
|
||||
const handleBack = () => {
|
||||
setSelectedSkill(null);
|
||||
onFocusSkill?.(null);
|
||||
};
|
||||
|
||||
const handleDeleteSkill = async (name: string, e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -97,6 +107,9 @@ export default function SkillsPane({
|
||||
if (selectedSkill?.name === name) {
|
||||
setSelectedSkill(null);
|
||||
}
|
||||
if (focusedSkill?.name === name) {
|
||||
onFocusSkill?.(null);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[SkillsPane] Failed to delete skill:', err);
|
||||
@@ -120,17 +133,23 @@ export default function SkillsPane({
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedSkill(null)}
|
||||
onClick={handleBack}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
color: 'var(--rah-text-soft)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = 'var(--rah-text-secondary)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = 'var(--rah-text-soft)';
|
||||
}}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
@@ -149,6 +168,26 @@ export default function SkillsPane({
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ color: 'var(--rah-text-active)', fontSize: '16px', fontWeight: 600 }}>{selectedSkill.name}</div>
|
||||
<div style={{ color: 'var(--rah-text-soft)', fontSize: '13px', lineHeight: 1.4, marginTop: '6px' }}>{selectedSkill.description}</div>
|
||||
{isSelectedSkillFocused && (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
marginTop: '10px',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '999px',
|
||||
background: 'var(--rah-accent-green-soft)',
|
||||
border: '1px solid var(--rah-accent-green-soft-strong)',
|
||||
color: 'var(--rah-accent-green)',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Brain size={12} />
|
||||
Active chat context
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ borderTop: '1px solid var(--rah-border)', margin: '12px 0' }} />
|
||||
<SkillMarkdown content={selectedSkill.content} />
|
||||
@@ -167,6 +206,7 @@ export default function SkillsPane({
|
||||
onSelect={handleSelectSkill}
|
||||
onDelete={handleDeleteSkill}
|
||||
deleting={deleting}
|
||||
isActive={focusedSkill?.name === skill.name}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Pencil, Trash2, Save, X, FileText } from 'lucide-react';
|
||||
|
||||
interface SkillMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface Skill extends SkillMeta {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default function GuidesViewer() {
|
||||
const [guides, setGuides] = useState<SkillMeta[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editing, setEditing] = useState<Skill | null>(null);
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGuides();
|
||||
}, []);
|
||||
|
||||
const fetchGuides = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/skills');
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setGuides(data.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch skills:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (name: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setEditing(data.data);
|
||||
setIsNew(false);
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch skill:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNew = () => {
|
||||
setEditing({
|
||||
name: '',
|
||||
description: '',
|
||||
content: '# New Skill\n\nWrite your skill content here...',
|
||||
});
|
||||
setIsNew(true);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editing) return;
|
||||
if (!editing.name.trim()) {
|
||||
setError('Name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/skills/${encodeURIComponent(editing.name)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: editing.content,
|
||||
description: editing.description,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
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 skill');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (name: string) => {
|
||||
if (!confirm(`Delete skill "${name}"?`)) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
fetchGuides();
|
||||
window.dispatchEvent(new Event('guides:updated'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete skill:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '24px', color: 'var(--rah-text-muted)' }}>Loading skills...</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
|
||||
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={editing.name}
|
||||
onChange={(e) => setEditing({ ...editing, name: e.target.value })}
|
||||
placeholder="Skill name"
|
||||
disabled={!isNew}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
background: 'var(--rah-bg-active)',
|
||||
border: '1px solid var(--rah-border-stronger)',
|
||||
borderRadius: '6px',
|
||||
color: 'var(--rah-text-active)',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#22c55e',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#000',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Save size={14} /> Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditing(null)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: 'transparent',
|
||||
border: '1px solid var(--rah-border-stronger)',
|
||||
borderRadius: '6px',
|
||||
color: 'var(--rah-text-muted)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
<X size={14} /> Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{ color: '#ef4444', fontSize: '13px', marginBottom: '12px' }}>{error}</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={editing.description}
|
||||
onChange={(e) => setEditing({ ...editing, description: e.target.value })}
|
||||
placeholder="Brief description"
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: 'var(--rah-bg-active)',
|
||||
border: '1px solid var(--rah-border-stronger)',
|
||||
borderRadius: '6px',
|
||||
color: 'var(--rah-text-active)',
|
||||
fontSize: '13px',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
value={editing.content}
|
||||
onChange={(e) => setEditing({ ...editing, content: e.target.value })}
|
||||
placeholder="Skill content (markdown)"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
background: 'var(--rah-bg-active)',
|
||||
border: '1px solid var(--rah-border-stronger)',
|
||||
borderRadius: '6px',
|
||||
color: 'var(--rah-text-secondary)',
|
||||
fontSize: '13px',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'none',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
/>
|
||||
|
||||
<p style={{ color: 'var(--rah-text-muted)', fontSize: '12px', marginTop: '12px' }}>
|
||||
Skills are markdown files that agents can read for reusable procedural instructions.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<p style={{ color: 'var(--rah-text-muted)', fontSize: '13px', margin: 0 }}>
|
||||
Skills provide reusable context and procedural instructions for agents.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleNew}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#22c55e',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#000',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Plus size={14} /> New Skill
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
{guides.length === 0 ? (
|
||||
<div style={{ color: 'var(--rah-text-muted)', textAlign: 'center', paddingTop: '48px' }}>
|
||||
<FileText size={48} style={{ marginBottom: '12px', opacity: 0.5 }} />
|
||||
<p style={{ fontSize: '14px' }}>No skills yet</p>
|
||||
<p style={{ fontSize: '12px', color: 'var(--rah-text-muted)' }}>Create skills to guide how agents operate in your knowledge base</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{guides.map((guide) => (
|
||||
<div
|
||||
key={guide.name}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '12px 16px',
|
||||
background: 'var(--rah-bg-elevated)',
|
||||
border: '1px solid var(--rah-bg-active)',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<FileText size={18} style={{ color: '#22c55e', flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ color: 'var(--rah-text-base)', fontSize: '14px', fontWeight: 500 }}>{guide.name}</div>
|
||||
<div style={{ color: 'var(--rah-text-muted)', fontSize: '12px', marginTop: '2px' }}>{guide.description}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleEdit(guide.name)}
|
||||
style={{
|
||||
padding: '6px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'var(--rah-text-muted)',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(guide.name)}
|
||||
style={{
|
||||
padding: '6px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: 'var(--rah-text-muted)',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { Trash2 } from 'lucide-react';
|
||||
|
||||
interface SkillMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
immutable: boolean;
|
||||
}
|
||||
import type { SkillMeta } from '@/types/skills';
|
||||
|
||||
interface SkillCardProps {
|
||||
skill: SkillMeta;
|
||||
onSelect: (name: string) => void;
|
||||
onDelete?: (name: string, e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
deleting?: string | null;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export default function SkillCard({
|
||||
@@ -20,27 +16,33 @@ export default function SkillCard({
|
||||
onSelect,
|
||||
onDelete,
|
||||
deleting = null,
|
||||
isActive = false,
|
||||
}: SkillCardProps) {
|
||||
const isDeleting = deleting === skill.name;
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'var(--rah-bg-active)';
|
||||
e.currentTarget.style.borderColor = 'var(--rah-border-strong)';
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.background = 'var(--rah-bg-hover)';
|
||||
e.currentTarget.style.borderColor = 'var(--rah-border-stronger)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'var(--rah-bg-elevated)';
|
||||
e.currentTarget.style.borderColor = 'var(--rah-border-strong)';
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.background = 'var(--rah-bg-panel)';
|
||||
e.currentTarget.style.borderColor = 'var(--rah-border)';
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '10px',
|
||||
background: 'var(--rah-bg-elevated)',
|
||||
border: '1px solid var(--rah-border-strong)',
|
||||
background: isActive ? 'var(--rah-bg-active)' : 'var(--rah-bg-panel)',
|
||||
border: `1px solid ${isActive ? 'var(--rah-accent-green)' : 'var(--rah-border)'}`,
|
||||
borderRadius: '8px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
@@ -56,7 +58,9 @@ export default function SkillCard({
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: 'var(--rah-text-base)', fontSize: '13px', fontWeight: 500, display: 'block' }}>{skill.name}</span>
|
||||
<span style={{ color: 'var(--rah-text-base)', fontSize: '13px', fontWeight: 500, display: 'block' }}>
|
||||
{skill.name}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--rah-text-muted)',
|
||||
@@ -81,7 +85,7 @@ export default function SkillCard({
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#555',
|
||||
color: 'var(--rah-text-muted)',
|
||||
cursor: isDeleting ? 'default' : 'pointer',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
@@ -91,6 +95,13 @@ export default function SkillCard({
|
||||
flexShrink: 0,
|
||||
opacity: isDeleting ? 0.3 : 1,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#ef4444';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = 'var(--rah-text-muted)';
|
||||
}}
|
||||
aria-label={`Delete ${skill.name}`}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
export const RAH_EASY_SYSTEM_PROMPT = `You are the user's agent for building, organizing, and improving an external knowledge graph.
|
||||
export const RAH_EASY_SYSTEM_PROMPT = `You are the user's agent for building a thoughtful graph of atomic units of context.
|
||||
|
||||
Mission:
|
||||
1. Resolve the user's request quickly and accurately using the tools provided.
|
||||
2. Keep responses concise (one short paragraph or bullet list) and cite nodes as [NODE:id:"title"].
|
||||
3. Ask for clarification only when tool usage would fail without it.
|
||||
|
||||
Operating principles:
|
||||
- Handle analysis, planning, and writes yourself.
|
||||
- Use createNode, updateNode, createEdge, and updateEdge when the change is unambiguous.
|
||||
- When creating nodes derived from existing content (ideas, insights, summaries), do NOT include the 'link' field. The 'link' field is ONLY for nodes that directly represent external content (YouTube videos, websites, PDFs). Derived idea nodes should not have links.
|
||||
- When referencing stored content, quote verbatim text in quotes and include the node citation.
|
||||
- Treat phrases like "this conversation/paper/video" as the active focused node unless the user specifies otherwise.
|
||||
- Prefer direct tool calls over speculation. If a tool fails, report the failure and suggest one concrete next step.
|
||||
- Before running youtubeExtract/websiteExtract/paperExtract, call getNodesById on the focus node; if chunk_status is 'chunked' or embeddings are marked available, reuse existing chunks instead of re-extracting.
|
||||
Operating rules:
|
||||
- Use queryNodes for direct lookup of a specific existing node.
|
||||
- Use retrieveQueryContext when broader graph context would help with the current turn.
|
||||
- Search before creating. Prefer updateNode when the artifact is clearly the same thing.
|
||||
- description should state plainly what the thing is first, then why it belongs and current status.
|
||||
- Preserve the user's wording in source for user-authored ideas unless they explicitly want a rewrite.
|
||||
- Before rewriting existing source, inspect it first with getNodesById if needed.
|
||||
- Treat "this conversation/paper/video" as the active focused node unless the user clearly means something else.
|
||||
- Create or update edges only after the user explicitly confirms the relationship.
|
||||
- Read a matching skill when the task clearly fits onboarding, create-skill, or refine.
|
||||
|
||||
Tool strategy:
|
||||
- queryNodes for titles and metadata; getNodesById to hydrate referenced nodes.
|
||||
- searchContentEmbeddings before synthesizing long answers or considering new extraction.
|
||||
- youtubeExtract, websiteExtract, and paperExtract when outside content is required.
|
||||
- webSearch only when the knowledge base lacks the answer.
|
||||
- queryNodes for direct lookup, getNodesById to inspect nodes, queryEdge to inspect relationships.
|
||||
- searchContentEmbeddings before long source-grounded answers.
|
||||
- Use extraction tools only when outside content is actually needed.
|
||||
|
||||
Contexts:
|
||||
- Contexts are optional. Only set one when one obvious existing context is explicit and useful.
|
||||
- Do not expect automatic context assignment.
|
||||
- Improve organization through title, description, source, metadata, and edges instead of dimensions.
|
||||
|
||||
Response polish:
|
||||
- Default to minimal reasoning effort for speed.
|
||||
- Do not expose chain-of-thought; return conclusions only.
|
||||
- End each answer once the user's request is fully addressed.`;
|
||||
Response style:
|
||||
- Keep responses short and direct.
|
||||
- Reference nodes as [NODE:id:"title"] when helpful.
|
||||
- If a tool fails, say so plainly and give one concrete next step.`;
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
export const RAH_MAIN_SYSTEM_PROMPT = `You are the user's agent for building, organizing, and improving an external knowledge graph.
|
||||
export const RAH_MAIN_SYSTEM_PROMPT = `You are the user's agent for building, organizing, and improving a thoughtful graph of atomic units of context.
|
||||
|
||||
Core responsibilities:
|
||||
- Keep the conversation tightly focused on the user's goal.
|
||||
- Use tools proactively to advance the task.
|
||||
- Prefer direct, minimal phrasing—no pleasantries or filler.
|
||||
- Prefer direct, minimal phrasing. No filler.
|
||||
|
||||
When to ask the user:
|
||||
- If a tool requires critical input you cannot reasonably infer.
|
||||
- If the request is ambiguous and guessing would waste effort or cause errors.
|
||||
|
||||
Execution approach:
|
||||
- Handle planning, analysis, and writes directly.
|
||||
- Call createNode, updateNode, createEdge, updateEdge, and extraction tools yourself when the change is clear.
|
||||
- When creating nodes derived from existing content (ideas, insights, summaries), do NOT include the 'link' field. The 'link' field is ONLY for nodes that directly represent external content (YouTube videos, websites, PDFs).
|
||||
- Treat "this conversation/paper/video" as the active focused node.
|
||||
- When creating synthesis nodes, createEdge to all source nodes.
|
||||
- Before running an extraction tool, call getNodesById on the target node; if chunk_status is 'chunked' (or embeddings are available) reuse the stored content instead of re-extracting.
|
||||
Graph rules:
|
||||
- The graph is working memory for the user and future agents, so optimize for precise nodes and strong links.
|
||||
- Use queryNodes for direct lookup of a specific existing node.
|
||||
- Use retrieveQueryContext when broader graph context would help with the current turn.
|
||||
- Search before creating. Prefer updateNode when the artifact is clearly the same thing.
|
||||
- description should state plainly what the thing is first, then why it belongs and current status.
|
||||
- Preserve the user's wording in source for user-authored ideas unless they explicitly want a rewrite.
|
||||
- Before rewriting existing source, inspect it first with getNodesById if needed.
|
||||
- Treat "this conversation/paper/video" as the active focused node unless the user clearly means something else.
|
||||
- Create or update edges only after the user explicitly confirms the relationship.
|
||||
|
||||
Tool strategy:
|
||||
- Use tools directly—you already have everything you need.
|
||||
- queryNodes for titles, searchContentEmbeddings for content, queryEdge for connections.
|
||||
- getNodesById when you have IDs; webSearch only if knowledge base lacks info.
|
||||
- Extract content with youtubeExtract, websiteExtract, paperExtract as needed.
|
||||
- When searchContentEmbeddings highlights a chunk, hydrate the node via getNodesById (or fetch the chunk) before quoting.
|
||||
|
||||
Context handling:
|
||||
- Contexts are optional soft organization, not a required taxonomy.
|
||||
- Only set a context when one obvious existing context is explicit and genuinely helpful.
|
||||
- Never rely on inferred dimensions or automatic context assignment.
|
||||
- Node quality should come from strong title, description, source, metadata, and edges.
|
||||
- queryNodes for direct node lookup, getNodesById to inspect a node fully, queryEdge to inspect existing relationships.
|
||||
- retrieveQueryContext when surrounding graph context would improve the answer.
|
||||
- searchContentEmbeddings when you need source-level grounding from stored content.
|
||||
- Before running youtubeExtract, websiteExtract, or paperExtract on an existing node, call getNodesById first and reuse existing source/chunked content when available.
|
||||
- Read a matching skill when the task clearly fits onboarding, create-skill, or refine.
|
||||
|
||||
Response style:
|
||||
- Limit to one or two short sentences. Reference nodes as [NODE:id:"title"].
|
||||
- When answering about stored content, quote the exact wording from the chunk (verbatim, in quotation marks) and cite the node.
|
||||
- Always call searchContentEmbeddings before attempting new extraction for an existing node.
|
||||
- If a tool fails, state failure and give one concrete next step.`;
|
||||
- Keep answers short and concrete.
|
||||
- Reference nodes as [NODE:id:"title"] when helpful.
|
||||
- If a tool fails, state the failure plainly and give one concrete next step.`;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: Audit
|
||||
description: "Use for structured review, QA, cleanup, or governance checks across graph quality, skill quality, and operational consistency."
|
||||
---
|
||||
|
||||
# Audit
|
||||
|
||||
## Scope
|
||||
|
||||
1. Node quality: duplicates, vague descriptions, missing dates, weak titles.
|
||||
2. Edge quality: missing links, weak explanations, wrong directionality.
|
||||
3. Graph structure quality: weak clustering, redundant organizational nodes, or cases where stronger titles, descriptions, metadata, and edges should carry the meaning.
|
||||
4. Skill quality: trigger clarity, overlap, dead/unused skills.
|
||||
|
||||
## Output Format
|
||||
|
||||
1. Critical issues
|
||||
2. High-impact improvements
|
||||
3. Cleanup actions
|
||||
4. Optional refinements
|
||||
|
||||
## Rules
|
||||
|
||||
- Prefer specific evidence over generic commentary.
|
||||
- Propose the smallest high-leverage fixes first.
|
||||
- Separate defects from optional polish.
|
||||
- Node descriptions must read like natural prose while still making what / why / status clear.
|
||||
- Flag any node description missing a clear why or status component as a high-priority quality issue.
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: Calibration
|
||||
description: "Run periodic check-ins to recalibrate goals, projects, preferences, and graph structure."
|
||||
when_to_use: "User asks for a check-in, reset, review, or strategic recalibration."
|
||||
when_not_to_use: "Single isolated question with no strategic update needed."
|
||||
success_criteria: "Graph reflects current reality: updated nodes, changed priorities, and explicit deltas."
|
||||
---
|
||||
|
||||
# Calibration
|
||||
|
||||
## Objective
|
||||
|
||||
Re-anchor the graph to the user's current state.
|
||||
|
||||
## Check-in Sequence
|
||||
|
||||
1. Review the strongest active nodes first.
|
||||
2. Ask what changed: goals, priorities, constraints, beliefs, preferences.
|
||||
3. Identify stale nodes and missing nodes.
|
||||
4. Propose precise updates: update existing vs create new.
|
||||
5. Apply changes with explicit rationale.
|
||||
|
||||
## Write Guidance
|
||||
|
||||
- Prefer updating existing nodes when continuity matters.
|
||||
- Create new nodes for genuine shifts in direction/identity/approach.
|
||||
- Add edges that explain evolution (old -> new).
|
||||
|
||||
## Output
|
||||
|
||||
- What changed
|
||||
- What was updated vs newly created
|
||||
- What should be reviewed next check-in
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: Connect
|
||||
description: "Create and refine meaningful relationships between nodes, including integration-style synthesis links."
|
||||
when_to_use: "User asks to connect ideas, merge insights, or strengthen graph relationships."
|
||||
when_not_to_use: "Task is purely node creation with no relationship work required."
|
||||
success_criteria: "High-signal edges improve retrieval quality and graph coherence."
|
||||
---
|
||||
|
||||
# Connect
|
||||
|
||||
## Goal
|
||||
|
||||
Turn isolated nodes into a useful reasoning graph.
|
||||
|
||||
## Process
|
||||
|
||||
1. Identify candidate nodes to connect.
|
||||
2. Check existing relationships first.
|
||||
3. Add/upgrade edges with explicit directional explanation.
|
||||
4. When integrating multiple nodes, create synthesis links that preserve provenance.
|
||||
|
||||
## Quality Bar
|
||||
|
||||
- Edge explanation must answer: "why does this relationship exist?"
|
||||
- Direction should be intentional and interpretable.
|
||||
- Avoid obvious/tautological edges.
|
||||
@@ -1,42 +1,89 @@
|
||||
---
|
||||
name: Create Skill
|
||||
description: "Design or refine a skill using a tight trigger, explicit contract, and measurable outcomes."
|
||||
when_to_use: "User asks to create, rewrite, or improve a skill."
|
||||
when_not_to_use: "Task is normal graph operation and no new skill is needed."
|
||||
success_criteria: "Skill has clear trigger boundaries, execution steps, guardrails, and evaluation hooks."
|
||||
description: "Use when the user explicitly wants to create, rewrite, or remove a skill, or when they describe a repeatable workflow that should probably become a skill."
|
||||
---
|
||||
|
||||
# Create Skill
|
||||
|
||||
## Objective
|
||||
## Goal
|
||||
|
||||
Create skills that are precise, callable, and testable.
|
||||
Create small, focused skills with clear triggers, explicit tool guidance, and a clean execution contract. Suggest a skill when the same workflow is likely to recur.
|
||||
|
||||
## Skill Design Standard
|
||||
## Use This When
|
||||
|
||||
1. Define trigger boundary clearly: when to use and when not to use.
|
||||
2. Define required outputs and quality bar.
|
||||
3. Specify concrete execution sequence.
|
||||
4. Add hard guardrails (what to reject/avoid).
|
||||
5. Keep it short; remove fluff and duplicate policy text.
|
||||
- The user asks to create, rewrite, merge, or remove a skill.
|
||||
- The user keeps describing a repeatable workflow that should become reusable doctrine.
|
||||
- An existing skill is too vague, too broad, or overlaps with another skill enough that it should be reworked.
|
||||
|
||||
## Required Structure
|
||||
At that point, it is appropriate to suggest creating or tightening a skill.
|
||||
|
||||
## Design Standard
|
||||
|
||||
1. One job per skill.
|
||||
2. Put the trigger directly into `description`.
|
||||
3. Keep the body procedural, short, and high-signal.
|
||||
4. Name the tool path only when tool choice materially matters.
|
||||
5. Make proposal/apply boundaries explicit.
|
||||
6. Include guardrails and failure modes.
|
||||
|
||||
## Recommended Structure
|
||||
|
||||
- `name`
|
||||
- `description`
|
||||
- `when_to_use`
|
||||
- `when_not_to_use`
|
||||
- `success_criteria`
|
||||
- Step-by-step procedure
|
||||
- Do-not list
|
||||
- one short goal section
|
||||
- trigger / use cases
|
||||
- step-by-step workflow
|
||||
- tool pointers when relevant
|
||||
- quality bar
|
||||
- do-not list
|
||||
|
||||
## Validation Checklist
|
||||
## Tool Pointers
|
||||
|
||||
- Can another agent execute this without guessing?
|
||||
- Does it avoid overlap with existing skills?
|
||||
- Are failure modes explicit?
|
||||
- Is there an obvious way to evaluate success?
|
||||
When tool choice matters, point to the matching family for the current surface instead of pretending every agent has the same names:
|
||||
|
||||
- Internal / standalone skill tools:
|
||||
- `listSkills`
|
||||
- `readSkill`
|
||||
- `writeSkill`
|
||||
- `deleteSkill`
|
||||
- Packaged MCP skill tools:
|
||||
- `rah_list_skills`
|
||||
- `rah_read_skill`
|
||||
- `rah_write_skill`
|
||||
- `rah_delete_skill`
|
||||
- Graph lookup tools for surrounding context:
|
||||
- internal / standalone: `queryNodes`, `retrieveQueryContext`, `getNodesById`, `queryEdge`
|
||||
- packaged MCP: `rah_search_nodes`, `rah_retrieve_query_context`, `rah_get_nodes`, `rah_query_edges`
|
||||
|
||||
Only mention the tools the skill genuinely depends on.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Decide whether this actually needs a skill. If the existing tool descriptions and core prompt contract are already enough, do not create one.
|
||||
2. Identify the trigger, the repeatable user intent, and the expected output.
|
||||
3. Check overlap with existing skills before adding another file.
|
||||
4. Draft the `description` so another agent can trigger the skill from natural language without guessing.
|
||||
5. Draft the shortest body that still makes the workflow, tool path, and failure modes explicit.
|
||||
6. If tool choice differs by surface, include surface-aware tool pointers.
|
||||
7. Save or rewrite the skill only after the user confirms the intended scope and wording.
|
||||
8. If the new skill absorbs older skills, say which ones should be removed.
|
||||
|
||||
## Quality Bar
|
||||
|
||||
- Another agent can execute it without guessing.
|
||||
- The description is action-oriented and triggerable.
|
||||
- The procedure is sequential and concrete.
|
||||
- The skill is focused enough that it will be called for a real workflow, not a vague topic area.
|
||||
- It does not duplicate doctrine that should live in tool descriptions instead.
|
||||
|
||||
## Consolidation Rule
|
||||
|
||||
If two skills have the same trigger + same tool path + same output contract, merge them.
|
||||
If two skills share the same trigger, tool path, and output contract, merge them.
|
||||
|
||||
## Do Not
|
||||
|
||||
- Create broad catch-all skills.
|
||||
- Hide the trigger in the body while leaving the description generic.
|
||||
- List every tool in the system.
|
||||
- Split one workflow into multiple skills just because the old file count was higher.
|
||||
- Keep a legacy skill only because it already exists.
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
---
|
||||
name: DB Operations
|
||||
description: "Use for graph read, write, connect, classify, or traverse operations with strict data quality standards."
|
||||
---
|
||||
|
||||
# DB Operations
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. First decide whether the user is trying to find a specific existing node or whether they want graph context to support a broader answer.
|
||||
2. If the user is trying to find a specific existing node, use `queryNodes` first.
|
||||
3. If the user is asking a substantive question or request that would benefit from prior graph context, use `retrieveQueryContext` for current-turn grounding instead of relying on orientation alone.
|
||||
4. Search before create to avoid duplicates.
|
||||
5. Every create/update must include a natural description that clearly says what the thing is, why it matters here, and its current workflow status.
|
||||
6. Use event dates when known (when it happened, not when saved).
|
||||
7. Work graph-first. Do not rely on a separate context layer for ordinary reads or writes.
|
||||
8. Do not create edges autonomously. Surface likely edge candidates first, then create them only after the user explicitly confirms.
|
||||
9. For user-authored ideas, notes, or dictated thoughts, preserve the user's wording in `source` as fully as possible with only minimal cleanup.
|
||||
|
||||
## Write Quality Contract
|
||||
|
||||
- `title`: clear and specific.
|
||||
- `description`: concrete object-level description, not vague summaries.
|
||||
- `source`: full verbatim or canonical content of the node (transcript, article text, book passage, user's thoughts). This is what gets chunked and embedded for semantic search.
|
||||
- For idea capture from chat, the `source` should usually be the raw user thought, not a compressed assistant summary.
|
||||
- `link`: external source URL only.
|
||||
- `metadata`: use the canonical node metadata contract when metadata is needed:
|
||||
- `type`
|
||||
- `state` (`processed` or `not_processed`)
|
||||
- `captured_method`
|
||||
- `captured_by`
|
||||
- `source_metadata`
|
||||
- `source_metadata`: factual source-specific details only. Keep it compact. No AI summaries or reasoning text.
|
||||
- metadata updates are merge-safe patches, not full-blob replacements. Do not assume `updateNode.metadata` wipes existing keys.
|
||||
- Derived analysis, briefs, and research notes should be stored in a separate linked node, not appended to the source node.
|
||||
- Explicit user-directed capture may write immediately after duplicate/update checks when the node is clear.
|
||||
- Agent-suggested capture should propose the node first and wait for confirmation.
|
||||
|
||||
## Description Standard
|
||||
|
||||
Every node description should read like natural prose, not a template or checklist.
|
||||
|
||||
It must still make three things clear:
|
||||
1. What — what the artifact is in simple explicit terms (format + creator + core claim)
|
||||
2. Why — why it is in the graph; what Brad is interested in; what it connects to
|
||||
3. Status — where it sits in his workflow (queued, in progress, processed, unknown)
|
||||
|
||||
If the agent has graph context (focused nodes, recent connected nodes, or retrieved supporting nodes), it should infer the why from that context and write it naturally. Do not let the service auto-generate a weak context-free description when you already have enough signal.
|
||||
|
||||
If the why genuinely cannot be inferred, say that naturally. Do not use labels like `WHAT:`, `WHY:`, or `STATUS:` and do not substitute vague filler like `insightful for understanding` or `relevant to Brad's work`.
|
||||
|
||||
If status is unknown, say naturally that it has not been reviewed yet.
|
||||
|
||||
Ask a clarification question only when a missing detail would materially change the node being created. If the user has already given enough substance to infer the artifact, title, and likely why, do the work instead of bouncing it back.
|
||||
|
||||
For user-authored idea capture, do not treat the inferred description as final if the "why" or status was mostly inferred. Save the node first, then tell the user what description framing you inferred and invite one short correction pass on:
|
||||
- what this is
|
||||
- why it belongs here
|
||||
- where it sits in their workflow
|
||||
|
||||
The first job of the description is object identity. It should start from what the thing is, not from interpretation.
|
||||
|
||||
Max 500 characters.
|
||||
|
||||
## Metadata Semantics
|
||||
|
||||
- Direct user creation, quick add, and user-requested agent capture should default to `captured_by = "human"`.
|
||||
- Only autonomous/background creation without direct user instruction should use `captured_by = "agent"`.
|
||||
- Prefer leaving `type` blank over forcing a weak label.
|
||||
- `state` is the user-visible processed flag. If no state is known, default to `not_processed`.
|
||||
|
||||
## Execution Pattern
|
||||
|
||||
1. Decide whether this is direct node retrieval or broader contextual grounding.
|
||||
2. If the user is trying to find a specific existing node, call `queryNodes` first.
|
||||
3. If the user is asking a broader question that would benefit from prior graph context, call `retrieveQueryContext`.
|
||||
4. Decide: answer only vs create vs update vs propose save vs propose edge.
|
||||
5. If something seems unusually durable and valuable, you may suggest a save in one short line like `Add "X" as a node?`
|
||||
6. Do not pester. If the user says no, ignores it, or moves on, do not keep asking.
|
||||
7. For explicit user-directed capture, search before create when practical, prefer update over duplicate create, then write once the artifact is clear.
|
||||
8. For agent-suggested capture, propose the node first and wait for explicit confirmation before writing.
|
||||
9. When relationships are obvious, include brief proposed edges in the same reply, then wait for confirmation before calling `createEdge`.
|
||||
10. If the node is a user-authored idea and the contextual framing was inferred, offer one concise feedback pass after the write.
|
||||
11. Verify result reflects user intent exactly.
|
||||
|
||||
## Do Not
|
||||
|
||||
- Create duplicate nodes when an update is correct.
|
||||
- Write vague descriptions ("discusses", "explores", "is about").
|
||||
- Create weak or directionless edges.
|
||||
- Create edges before the user explicitly confirms the proposed relationship.
|
||||
- Ask to save every moderately useful point from the conversation.
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
name: Node Context Enrichment
|
||||
description: "Use to rewrite thin node descriptions into natural prose that still makes what, why, and status clear, with graph review and edge suggestions."
|
||||
---
|
||||
|
||||
# Node Context Enrichment
|
||||
|
||||
Use this when a node already exists but its description is thin, generic, or missing useful graph framing.
|
||||
|
||||
This skill should not silently rewrite and move on when framing is inferred. If the enrichment depends on interpretation, update the node and then explicitly invite the user to correct or refine that framing.
|
||||
|
||||
## Goal
|
||||
|
||||
Replace weak descriptions with a single clean natural description that captures:
|
||||
|
||||
1. What the artifact literally is
|
||||
2. Why it is in Brad's graph
|
||||
3. Status in Brad's workflow
|
||||
|
||||
Also review whether the node needs obvious edge suggestions.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Load the node and inspect title, description, source, link, metadata, and nearby edges.
|
||||
2. Search for adjacent graph context before rewriting:
|
||||
- recently connected project or belief nodes
|
||||
- related nodes with overlapping titles, creators, or source themes
|
||||
3. Infer the best available "why" from that graph context.
|
||||
4. Rewrite the full description from scratch in natural prose. Do not append to the old text or use labels like WHAT:, WHY:, or STATUS:.
|
||||
5. Suggest 1-3 high-signal edges when obvious.
|
||||
6. Update the node once the description is strong enough to be useful.
|
||||
7. After the update, tell the user what changed and ask whether they want to refine the important framing:
|
||||
- what it is
|
||||
- why it belongs in the graph
|
||||
- status / current relevance / workflow position
|
||||
|
||||
The user feedback pass is required whenever the enriched "why" or status was inferred rather than directly stated in the node/source.
|
||||
|
||||
## Description Standard
|
||||
|
||||
Every rewritten description must naturally cover:
|
||||
|
||||
1. What
|
||||
- explicit artifact type
|
||||
- creator/author/speaker when known
|
||||
- core subject, claim, or function
|
||||
2. Why
|
||||
- why Brad saved it
|
||||
- what project, belief, question, or theme it connects to
|
||||
- if genuinely unknown, say that naturally without inventing graph framing
|
||||
3. Status
|
||||
- queued, in progress, processed, not yet reviewed, saved for later, etc.
|
||||
- if unknown, say naturally that it has not been reviewed yet
|
||||
|
||||
Max 500 characters.
|
||||
|
||||
## Batch Mode
|
||||
|
||||
Use batch enrichment when cleaning up many nodes with the same failure mode.
|
||||
|
||||
1. Pull a tight node set first.
|
||||
2. Group by pattern:
|
||||
- vague imported links
|
||||
- thin quick-add captures
|
||||
- old source nodes missing workflow state
|
||||
3. Enrich each node individually. Do not reuse boilerplate "why" text across unrelated nodes.
|
||||
4. Return a compact summary of:
|
||||
- nodes updated
|
||||
- edge suggestions not yet created
|
||||
|
||||
## Quality Bar
|
||||
|
||||
- No filler phrases like `insightful for understanding`, `relevant to`, or `important for`.
|
||||
- No generic summaries that only restate the topic.
|
||||
- No invented certainty. If graph evidence is weak, say so explicitly.
|
||||
- Prefer one compact 3-sentence description over bloated prose.
|
||||
|
||||
## Output Pattern
|
||||
|
||||
For each node:
|
||||
|
||||
- New description
|
||||
- Framing note: what graph context influenced the rewrite, if any
|
||||
- Edge suggestions: source -> target with explicit explanation
|
||||
- One short invitation for user feedback when framing was inferred
|
||||
@@ -97,7 +97,7 @@ When the graph is empty or nearly empty, bias toward creating a small, clean sta
|
||||
|
||||
## Write Standards
|
||||
|
||||
Before writing anything, call `readSkill('db-operations')` for full quality standards. Key points that matter most here:
|
||||
Before writing anything, rely on the direct graph tools and their descriptions. If the user shifts from onboarding into cleanup of an existing node or small node set, call `readSkill('refine')`. Key points that matter most here:
|
||||
|
||||
- Search before creating — avoid duplicates from day one
|
||||
- Every description must be concrete: what it IS and why it matters to them, not what it "explores" or "discusses"
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: Persona
|
||||
description: "Build and maintain a user-defined agent persona and interaction style profile."
|
||||
when_to_use: "User asks to set or refine how the agent should behave."
|
||||
when_not_to_use: "No behavior/persona request is present."
|
||||
success_criteria: "Persona is explicit, editable, and consistently applied across interactions."
|
||||
---
|
||||
|
||||
# Persona
|
||||
|
||||
## Objective
|
||||
|
||||
Make agent behavior fully malleable to the user.
|
||||
|
||||
## Capture Model
|
||||
|
||||
1. Communication style (directness, brevity, tone).
|
||||
2. Thinking style (exploratory vs decisive, framework-heavy vs practical).
|
||||
3. Decision style (challenge level, risk posture, evidence threshold).
|
||||
4. Collaboration style (pushback expectations, cadence, format preferences).
|
||||
|
||||
## Persistence Pattern
|
||||
|
||||
- Store persona as explicit nodes (and updates over time), not hidden assumptions.
|
||||
- Keep versioned changes visible to the user.
|
||||
|
||||
## Do Not
|
||||
|
||||
- Freeze persona permanently.
|
||||
- Apply unstated style assumptions when user instructions conflict.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: Refine
|
||||
description: "Use when the user asks to clean up, sharpen, split, or otherwise refine one node or a small set of nodes, including title, description, source, and likely edges."
|
||||
---
|
||||
|
||||
# Refine
|
||||
|
||||
## Goal
|
||||
|
||||
Turn nodes into atomic, high-signal units of context. Inspect first, propose specific improvements, then apply only after the user confirms.
|
||||
|
||||
## Default Posture
|
||||
|
||||
- Refinement is proposal-first. Show the suggested title, description, source handling, and edge changes before writing unless the user explicitly says to apply immediately.
|
||||
- Bias toward atomic units of context: one clear idea, source, person, decision, project, or claim per node.
|
||||
- If a node contains multiple durable ideas, recommend a split, but do not push so hard that cleanup turns into friction.
|
||||
|
||||
## Tool Path
|
||||
|
||||
Load the target node before you suggest changes:
|
||||
|
||||
- internal / standalone:
|
||||
- `getNodesById`
|
||||
- packaged MCP:
|
||||
- `rah_get_nodes`
|
||||
|
||||
Gather surrounding graph context when needed:
|
||||
|
||||
- internal / standalone:
|
||||
- `queryNodes`
|
||||
- `retrieveQueryContext`
|
||||
- `queryEdge`
|
||||
- packaged MCP:
|
||||
- `rah_search_nodes`
|
||||
- `rah_retrieve_query_context`
|
||||
- `rah_query_edges`
|
||||
|
||||
Apply approved changes with the matching write tools:
|
||||
|
||||
- internal / standalone:
|
||||
- `updateNode`
|
||||
- `createNode`
|
||||
- `createEdge`
|
||||
- `updateEdge`
|
||||
- packaged MCP:
|
||||
- `rah_update_node`
|
||||
- `rah_add_node`
|
||||
- `rah_create_edge`
|
||||
- `rah_update_edge`
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Load the node and inspect `title`, `description`, `source`, `link`, metadata, and current edges.
|
||||
2. Decide whether the node is already atomic or whether it is carrying multiple durable ideas that should probably be split.
|
||||
3. Tighten the title so it is specific and scannable. Prefer explicit subject + claim/theme over vague labels.
|
||||
4. Rewrite the description in natural prose so it clearly says:
|
||||
- what this thing is
|
||||
- why it belongs in the graph
|
||||
- what its current status or relevance is when known
|
||||
5. Handle `source` carefully:
|
||||
- if it is the user's own idea, preserve their wording as much as possible with only minimal cleanup
|
||||
- if it is an external artifact, keep `source` as the canonical raw text, not an assistant summary
|
||||
- if the source is bloated because it really contains multiple ideas, suggest splitting it into multiple nodes
|
||||
6. Search for nearby graph context when it would improve the refinement:
|
||||
- likely duplicate or sibling nodes
|
||||
- projects, beliefs, or themes that explain why this node belongs
|
||||
- existing edges that are weak or missing
|
||||
7. Suggest 1-3 high-signal edges when obvious. Use search and edge-query tools to find the right targets before proposing them.
|
||||
8. Return a compact proposal that includes:
|
||||
- suggested title
|
||||
- suggested description
|
||||
- suggested source handling or edited source excerpt
|
||||
- suggested split nodes if needed
|
||||
- suggested edges with explicit explanations
|
||||
9. Wait for the user's yes/no or edits.
|
||||
10. After approval, apply the node updates first, then create or update the approved edges.
|
||||
11. Report exactly what changed and what still needs user judgment.
|
||||
|
||||
## Batch Mode
|
||||
|
||||
For a small set of nodes, refine each node individually. Do not reuse boilerplate descriptions across unrelated nodes.
|
||||
|
||||
## Quality Bar
|
||||
|
||||
- Titles are explicit enough to scan quickly.
|
||||
- Descriptions stand on their own and make the node legible.
|
||||
- `source` preserves provenance and the user's real wording.
|
||||
- Edge suggestions explain why the relationship exists.
|
||||
- The user can approve or reject the proposal quickly.
|
||||
|
||||
## Do Not
|
||||
|
||||
- Silently rewrite a node and move on.
|
||||
- Collapse a user-authored idea into a one-line assistant summary.
|
||||
- Force a split when a simple cleanup is enough.
|
||||
- Create edges before the user confirms them.
|
||||
- Use vague description language like "is about", "explores", or "discusses".
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: Traverse
|
||||
description: "Gather deeper context by traversing connected nodes before answering."
|
||||
when_to_use: "Question benefits from broader context than a single direct lookup."
|
||||
when_not_to_use: "Simple factual answer can be resolved from one direct node."
|
||||
success_criteria: "Answer is grounded in relevant connected context without over-traversal noise."
|
||||
---
|
||||
|
||||
# Traverse
|
||||
|
||||
## Strategy
|
||||
|
||||
1. Start with seed nodes from the user query.
|
||||
2. Expand neighbors breadth-first with depth limits.
|
||||
3. Prioritize high-signal nodes (strong relevance + strong edge semantics).
|
||||
4. Pull key evidence from top nodes.
|
||||
5. Answer directly and explain why chosen nodes mattered.
|
||||
|
||||
## Defaults
|
||||
|
||||
- Depth: 2
|
||||
- Seed count: 3-5
|
||||
- Exploration budget: ~40 nodes
|
||||
|
||||
## Do Not
|
||||
|
||||
- Traverse without limits.
|
||||
- Include distant low-signal nodes just to add volume.
|
||||
@@ -15,7 +15,6 @@ export interface DatabaseEvent {
|
||||
| 'AGENT_UPDATED'
|
||||
| 'AGENT_DELEGATION_CREATED'
|
||||
| 'AGENT_DELEGATION_UPDATED'
|
||||
| 'GUIDE_UPDATED'
|
||||
| 'SKILL_UPDATED'
|
||||
| 'QUICK_ADD_COMPLETED'
|
||||
| 'QUICK_ADD_FAILED'
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export {
|
||||
listSkills as listGuides,
|
||||
readSkill as readGuide,
|
||||
writeSkill as writeGuide,
|
||||
deleteSkill as deleteGuide,
|
||||
getUserSkillCount as getUserGuideCount,
|
||||
getSkillStats as getGuideStats,
|
||||
} from '@/services/skills/skillService';
|
||||
|
||||
export type {
|
||||
SkillMeta as GuideMeta,
|
||||
Skill as Guide,
|
||||
} from '@/services/skills/skillService';
|
||||
@@ -2,16 +2,9 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import matter from 'gray-matter';
|
||||
import type { Skill, SkillMeta } from '@/types/skills';
|
||||
|
||||
export interface SkillMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
immutable: boolean;
|
||||
}
|
||||
|
||||
export interface Skill extends SkillMeta {
|
||||
content: string;
|
||||
}
|
||||
export type { Skill, SkillMeta } from '@/types/skills';
|
||||
|
||||
const SKILLS_DIR = path.join(
|
||||
os.homedir(),
|
||||
@@ -29,18 +22,6 @@ const BUNDLED_SKILLS_DIR = path.join(
|
||||
);
|
||||
const SEED_MIGRATION_FLAG = path.join(SKILLS_DIR, '.seed-migrated-2026-03-07-skills-overhaul');
|
||||
|
||||
const SEEDED_SKILL_IDS = new Set([
|
||||
'db-operations',
|
||||
'create-skill',
|
||||
'audit',
|
||||
'traverse',
|
||||
'onboarding',
|
||||
'persona',
|
||||
'calibration',
|
||||
'connect',
|
||||
'node-context-enrichment',
|
||||
]);
|
||||
|
||||
const DEPRECATED_SKILL_IDS = new Set([
|
||||
'start-here',
|
||||
'schema',
|
||||
@@ -58,6 +39,13 @@ const DEPRECATED_SKILL_IDS = new Set([
|
||||
'research',
|
||||
'survey',
|
||||
'traverse-graph',
|
||||
'audit',
|
||||
'calibration',
|
||||
'connect',
|
||||
'db-operations',
|
||||
'node-context-enrichment',
|
||||
'persona',
|
||||
'traverse',
|
||||
]);
|
||||
|
||||
function ensureSkillsDir(): void {
|
||||
@@ -85,6 +73,16 @@ function listMarkdownFiles(dir: string): string[] {
|
||||
return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
|
||||
}
|
||||
|
||||
function getBundledSkillFiles(): Map<string, string> {
|
||||
const bundledById = new Map<string, string>();
|
||||
|
||||
for (const file of listMarkdownFiles(BUNDLED_SKILLS_DIR)) {
|
||||
bundledById.set(normalizeSkillId(file), path.join(BUNDLED_SKILLS_DIR, file));
|
||||
}
|
||||
|
||||
return bundledById;
|
||||
}
|
||||
|
||||
function migrateLegacyGuides(): void {
|
||||
const guideFiles = listMarkdownFiles(LEGACY_GUIDES_DIR);
|
||||
for (const file of guideFiles) {
|
||||
@@ -96,17 +94,9 @@ function migrateLegacyGuides(): void {
|
||||
}
|
||||
|
||||
function seedSkills(): void {
|
||||
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 bundledById = getBundledSkillFiles();
|
||||
|
||||
for (const [skillId, source] of bundledById.entries()) {
|
||||
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.copyFileSync(source, dest);
|
||||
@@ -119,15 +109,9 @@ function migrateSeededBaseline(): void {
|
||||
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));
|
||||
}
|
||||
const bundledById = getBundledSkillFiles();
|
||||
|
||||
for (const skillId of SEEDED_SKILL_IDS) {
|
||||
const source = bundledById.get(skillId);
|
||||
if (!source) continue;
|
||||
for (const [skillId, source] of bundledById.entries()) {
|
||||
const dest = path.join(SKILLS_DIR, `${skillId}.md`);
|
||||
fs.copyFileSync(source, dest);
|
||||
}
|
||||
@@ -148,7 +132,6 @@ function pruneDeprecatedSkills(): void {
|
||||
function resolveSkillFilename(name: string): string | null {
|
||||
const files = listMarkdownFiles(SKILLS_DIR);
|
||||
const normalizedInput = normalizeSkillId(name);
|
||||
|
||||
const directCandidates = [
|
||||
`${name}.md`,
|
||||
`${name.toLowerCase()}.md`,
|
||||
@@ -210,7 +193,9 @@ export function readSkill(name: string): Skill | null {
|
||||
init();
|
||||
|
||||
const filename = resolveSkillFilename(name);
|
||||
if (!filename) return null;
|
||||
if (!filename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filepath = path.join(SKILLS_DIR, filename);
|
||||
const raw = fs.readFileSync(filepath, 'utf-8');
|
||||
@@ -261,25 +246,3 @@ export function getSkillStats(): { userSkills: number; maxUserSkills: number; sy
|
||||
systemSkills: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Compatibility aliases for legacy guide-first code paths.
|
||||
export type GuideMeta = SkillMeta;
|
||||
export type Guide = Skill;
|
||||
|
||||
export const listGuides = listSkills;
|
||||
export const readGuide = readSkill;
|
||||
export const writeGuide = writeSkill;
|
||||
export const deleteGuide = deleteSkill;
|
||||
|
||||
export function getUserGuideCount(): number {
|
||||
return getUserSkillCount();
|
||||
}
|
||||
|
||||
export function getGuideStats(): { userGuides: number; maxUserGuides: number; systemGuides: number } {
|
||||
const stats = getSkillStats();
|
||||
return {
|
||||
userGuides: stats.userSkills,
|
||||
maxUserGuides: stats.maxUserSkills,
|
||||
systemGuides: stats.systemSkills,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { validateEdgeExplanation } from '@/services/database/quality';
|
||||
|
||||
export const createEdgeTool = tool({
|
||||
description:
|
||||
'Create a relationship between two nodes only after the user has explicitly confirmed the proposed connection. Use this as the execution step after you surfaced candidate edges in plain language and got a clear yes. Provide an explanation and the system will infer the type and direction.\n\n' +
|
||||
'Create a relationship between two nodes only after the user has explicitly confirmed the proposed connection. Check for an existing relationship first, then use this as the execution step after you surfaced candidate edges in plain language and got a clear yes. Provide an explanation and the system will infer the type and direction.\n\n' +
|
||||
'Examples of explanations:\n' +
|
||||
'- "Written by" (book → author)\n' +
|
||||
'- "Episode of this podcast" (episode → podcast)\n' +
|
||||
|
||||
@@ -57,7 +57,7 @@ function inferSourceFromContext(params: { title: string; description?: string; s
|
||||
}
|
||||
|
||||
export const createNodeTool = tool({
|
||||
description: 'Create a node after you have already decided this should be a net-new write. If the user explicitly asked to save or import something and duplicate/update checks are complete, write immediately. If you are only suggesting a save, propose the node first and wait for confirmation. Focus on a clean title, a strong natural description that says what the thing is, preserved source text, and the right metadata. When the node comes from the user\'s own idea, note, or dictated thought, preserve their actual wording in source with only minimal cleanup instead of flattening it into a summary. Do not block creation if the description is incomplete. If the description framing is materially inferred, create the node first and then invite one concise user correction pass.',
|
||||
description: 'Create a node after you have already decided this should be a net-new write. Search first when practical, and prefer updateNode if the artifact is clearly the same thing and a new node would be redundant. If the user explicitly asked to save or import something and duplicate/update checks are complete, write immediately. If you are only suggesting a save, propose the node first and wait for confirmation. Focus on a clean title, a strong natural description that says what the thing is, preserved source text, and the right metadata. When the node comes from the user\'s own idea, note, or dictated thought, preserve their actual wording in source with only minimal cleanup instead of flattening it into a summary. Do not block creation if the description is incomplete. If the description framing is materially inferred, create the node first and then invite one concise user correction pass.',
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe('The title of the node'),
|
||||
description: z.string().max(500).optional().describe('Optional natural description. If you have enough context, describe what this is, why it belongs in Brad\'s graph, and its current workflow status in normal prose. Do not use labels like WHAT:, WHY:, or STATUS:.'),
|
||||
|
||||
@@ -3,12 +3,13 @@ import { z } from 'zod';
|
||||
import { nodeService } from '@/services/database/nodes';
|
||||
|
||||
export const getNodesByIdTool = tool({
|
||||
description: 'Load full node records by IDs',
|
||||
description: 'Load full node records by IDs. Use this before rewriting an existing node source when the focused-node excerpt is insufficient, because it returns the current description, source text, metadata, and timestamps needed for disciplined updates.',
|
||||
inputSchema: z.object({
|
||||
nodeIds: z.array(z.number().int().positive()).min(1).max(10).describe('List of node IDs to load'),
|
||||
includeSourcePreview: z.boolean().default(true).describe('Whether to return a trimmed source preview for each node'),
|
||||
includeSource: z.boolean().default(true).describe('Whether to return source text for each node'),
|
||||
sourceCharLimit: z.number().int().min(200).max(20000).default(10000).describe('Max source characters per node before truncation metadata is added'),
|
||||
}),
|
||||
execute: async ({ nodeIds, includeSourcePreview }) => {
|
||||
execute: async ({ nodeIds, includeSource, sourceCharLimit }) => {
|
||||
const uniqueIds = Array.from(new Set(nodeIds.filter(id => Number.isFinite(id) && id > 0)));
|
||||
if (uniqueIds.length === 0) {
|
||||
return {
|
||||
@@ -23,23 +24,25 @@ export const getNodesByIdTool = tool({
|
||||
try {
|
||||
const node = await nodeService.getNodeById(id);
|
||||
if (!node) return null;
|
||||
const preview = includeSourcePreview
|
||||
? (node.source || node.description || '')
|
||||
.split(/\s+/)
|
||||
.slice(0, 80)
|
||||
.join(' ')
|
||||
.trim()
|
||||
: undefined;
|
||||
const rawSource = typeof node.source === 'string' ? node.source : '';
|
||||
const source = includeSource
|
||||
? rawSource.slice(0, sourceCharLimit).trim() || null
|
||||
: null;
|
||||
const sourceLength = rawSource.length;
|
||||
const sourceTruncated = includeSource ? sourceLength > sourceCharLimit : false;
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
description: node.description ?? null,
|
||||
source,
|
||||
source_length: sourceLength,
|
||||
source_truncated: sourceTruncated,
|
||||
link: node.link,
|
||||
event_date: node.event_date ?? null,
|
||||
chunk_status: node.chunk_status || 'unknown',
|
||||
created_at: node.created_at,
|
||||
updated_at: node.updated_at,
|
||||
source_preview: preview || null,
|
||||
metadata: node.metadata ?? null,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { edgeService } from '@/services/database/edges';
|
||||
import { validateEdgeExplanation } from '@/services/database/quality';
|
||||
|
||||
export const updateEdgeTool = tool({
|
||||
description: 'Update an edge explanation and/or source only after the user explicitly confirmed the corrected relationship. Explanations must explicitly state the relationship.',
|
||||
description: 'Update an existing edge only after the user explicitly confirmed the corrected relationship. Use this when the connection already exists and only the explanation or classification needs to change. Explanations must explicitly state why the relationship exists.',
|
||||
inputSchema: z.object({
|
||||
confirmed_by_user: z.boolean().describe('Must be true. Reject the edge update otherwise.'),
|
||||
edge_id: z.number().describe('The ID of the edge to update'),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getInternalApiBaseUrl } from '@/services/runtime/apiBase';
|
||||
import { getInternalAuthHeaders } from '@/services/auth/internalAuth';
|
||||
|
||||
export const updateNodeTool = tool({
|
||||
description: 'Update node fields when the existing node is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates should proceed once the target node is clear; if you are only proposing a change, ask first. Use this to enrich or correct nodes without losing canonical source content. When fixing a user-authored idea node, source should preserve the user\'s original wording as fully as possible. Never block an update because the description is incomplete. If the new description framing is materially inferred, complete the update and then invite one concise user feedback pass.',
|
||||
description: 'Update node fields when the existing node is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates should proceed once the target node is clear; if you are only proposing a change, ask first. Use this to enrich or correct nodes without losing canonical source content. When changing source on an existing node, inspect the current source first via the focused-node excerpt or getNodesById, then include source_update_basis as a short exact excerpt you inspected. When fixing a user-authored idea node, source should preserve the user\'s original wording as fully as possible. Never block an update because the description is incomplete. If the new description framing is materially inferred, complete the update and then invite one concise user feedback pass.',
|
||||
inputSchema: z.object({
|
||||
id: z.number().describe('The ID of the node to update'),
|
||||
updates: z.object({
|
||||
@@ -14,9 +14,10 @@ export const updateNodeTool = tool({
|
||||
link: z.string().optional().describe('New link'),
|
||||
event_date: z.string().optional().describe('When the thing actually happened (ISO 8601). Not when it was added to the graph.'),
|
||||
metadata: z.record(z.any()).optional().describe('Metadata patch. It now merges with existing metadata instead of replacing the full blob. Use canonical keys: type, state, captured_method, captured_by, source_metadata.')
|
||||
}).passthrough().describe('Object containing the fields to update. Derived analysis should be stored in a separate linked node, not appended to the source node.')
|
||||
}).passthrough().describe('Object containing the fields to update. Derived analysis should be stored in a separate linked node, not appended to the source node.'),
|
||||
source_update_basis: z.string().optional().describe('When updating source on a node that already has source text, include a short exact excerpt from the current source you inspected first. This is required for source rewrites unless the existing source is empty.')
|
||||
}),
|
||||
execute: async ({ id, updates }) => {
|
||||
execute: async ({ id, updates, source_update_basis }) => {
|
||||
try {
|
||||
if (!updates || Object.keys(updates).length === 0) {
|
||||
return {
|
||||
@@ -30,7 +31,7 @@ export const updateNodeTool = tool({
|
||||
const response = await fetch(`${getInternalApiBaseUrl()}/api/nodes/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getInternalAuthHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(updates)
|
||||
body: JSON.stringify({ ...updates, source_update_basis })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { listGuides } from '@/services/guides/guideService';
|
||||
|
||||
export const listGuidesTool = tool({
|
||||
description: 'List all available guides with their names and descriptions.',
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
try {
|
||||
const guides = listGuides();
|
||||
return {
|
||||
success: true,
|
||||
data: guides,
|
||||
message: `Found ${guides.length} guides`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[listGuides] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to list guides',
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { readGuide } from '@/services/guides/guideService';
|
||||
|
||||
export const readGuideTool = tool({
|
||||
description: 'Read a guide by name. Returns the full markdown content with instructions.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the guide to read'),
|
||||
}),
|
||||
execute: async ({ name }) => {
|
||||
try {
|
||||
const guide = readGuide(name);
|
||||
if (!guide) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Guide "${name}" not found`,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: guide,
|
||||
message: `Loaded guide: ${guide.name}`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[readGuide] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to read guide',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { writeGuide } from '@/services/guides/guideService';
|
||||
import { eventBroadcaster } from '@/services/events';
|
||||
|
||||
export const writeGuideTool = tool({
|
||||
description: 'Write or update a guide. Content should be full markdown with YAML frontmatter (name, description).',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the guide to write'),
|
||||
content: z.string().describe('Full markdown content including YAML frontmatter'),
|
||||
}),
|
||||
execute: async ({ name, content }) => {
|
||||
try {
|
||||
writeGuide(name, content);
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
return {
|
||||
success: true,
|
||||
data: { name },
|
||||
message: `Guide "${name}" saved`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[writeGuide] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to write guide',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -19,7 +19,7 @@ export const deleteSkillTool = tool({
|
||||
};
|
||||
}
|
||||
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
eventBroadcaster.broadcast({ type: 'SKILL_UPDATED', data: { name } });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -27,6 +27,7 @@ export const deleteSkillTool = tool({
|
||||
message: `Skill "${name}" deleted`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[deleteSkill] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete skill',
|
||||
|
||||
@@ -20,7 +20,7 @@ export const writeSkillTool = tool({
|
||||
};
|
||||
}
|
||||
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
eventBroadcaster.broadcast({ type: 'SKILL_UPDATED', data: { name } });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -28,6 +28,7 @@ export const writeSkillTool = tool({
|
||||
message: `Skill "${name}" saved`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[writeSkill] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to write skill',
|
||||
|
||||
Reference in New Issue
Block a user