feat(rah-light): replace workflows with guides system

- Remove entire workflow system (API routes, services, tools, components)
- Add guides system from main ra-h repo (for external agent integration)
- Update pane types: 'workflows' → 'guides' throughout
- Update LeftToolbar to show guides icon instead of workflows
- Update SettingsModal with GuidesViewer tab
- Add GuidesPane for browsing guides in main UI
- Clean up prompts to remove workflow references
- Update tool registry to remove workflow tools
- Add gray-matter package for frontmatter parsing

Guides are essential for RA-H Light as they help external agents
(via MCP) understand the knowledge base context and usage patterns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-01-29 16:29:24 +11:00
co-authored by Claude Opus 4.5
parent f383d770ca
commit b31441b35f
46 changed files with 1126 additions and 2189 deletions
+27
View File
@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from 'next/server';
import { readGuide } from '@/services/guides/guideService';
export const runtime = 'nodejs';
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ name: string }> }
) {
try {
const { name } = await params;
const guide = readGuide(name);
if (!guide) {
return NextResponse.json(
{ success: false, error: `Guide "${name}" not found` },
{ status: 404 }
);
}
return NextResponse.json({ success: true, data: guide });
} catch (error) {
console.error('[API /guides/[name]] error:', error);
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Failed to read guide' },
{ status: 500 }
);
}
}
+17
View File
@@ -0,0 +1,17 @@
import { NextResponse } from 'next/server';
import { listGuides } from '@/services/guides/guideService';
export const runtime = 'nodejs';
export async function GET() {
try {
const guides = listGuides();
return NextResponse.json({ success: true, data: guides });
} catch (error) {
console.error('[API /guides] error:', error);
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Failed to list guides' },
{ status: 500 }
);
}
}
-132
View File
@@ -1,132 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { saveWorkflow, deleteWorkflow, userWorkflowExists } from '@/services/workflows/workflowFileService';
import { WorkflowRegistry, BUNDLED_WORKFLOW_KEYS } from '@/services/workflows/registry';
// PUT /api/workflows/[key] - Create or update a workflow
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ key: string }> }
) {
try {
const { key } = await params;
const body = await request.json();
// Validate required fields
if (!body.displayName || typeof body.displayName !== 'string') {
return NextResponse.json(
{ success: false, error: 'displayName is required' },
{ status: 400 }
);
}
if (!body.instructions || typeof body.instructions !== 'string') {
return NextResponse.json(
{ success: false, error: 'instructions is required' },
{ status: 400 }
);
}
// Save the workflow
saveWorkflow({
key,
displayName: body.displayName.trim(),
description: body.description?.trim() || '',
instructions: body.instructions,
enabled: body.enabled !== false,
requiresFocusedNode: body.requiresFocusedNode !== false,
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error saving workflow:', error);
return NextResponse.json(
{ success: false, error: 'Failed to save workflow' },
{ status: 500 }
);
}
}
// DELETE /api/workflows/[key] - Delete a workflow (resets to default if bundled)
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ key: string }> }
) {
try {
const { key } = await params;
// Check if user file exists
if (!userWorkflowExists(key)) {
// If it's a bundled workflow, nothing to delete
if (BUNDLED_WORKFLOW_KEYS.has(key)) {
return NextResponse.json(
{ success: false, error: 'Cannot delete bundled workflow (no user override exists)' },
{ status: 400 }
);
}
return NextResponse.json(
{ success: false, error: 'Workflow not found' },
{ status: 404 }
);
}
const deleted = deleteWorkflow(key);
if (!deleted) {
return NextResponse.json(
{ success: false, error: 'Failed to delete workflow' },
{ status: 500 }
);
}
// If this was a bundled workflow, it will now fall back to default
const isBundled = BUNDLED_WORKFLOW_KEYS.has(key);
return NextResponse.json({
success: true,
resetToDefault: isBundled,
});
} catch (error) {
console.error('Error deleting workflow:', error);
return NextResponse.json(
{ success: false, error: 'Failed to delete workflow' },
{ status: 500 }
);
}
}
// GET /api/workflows/[key] - Get a single workflow
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ key: string }> }
) {
try {
const { key } = await params;
const workflow = await WorkflowRegistry.getWorkflowByKey(key);
if (!workflow) {
return NextResponse.json(
{ success: false, error: 'Workflow not found' },
{ status: 404 }
);
}
// Include metadata about whether it's user-modified
const hasUserOverride = userWorkflowExists(key);
const isBundled = BUNDLED_WORKFLOW_KEYS.has(key);
return NextResponse.json({
success: true,
data: {
...workflow,
isBundled,
hasUserOverride,
},
});
} catch (error) {
console.error('Error fetching workflow:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch workflow' },
{ status: 500 }
);
}
}
-146
View File
@@ -1,146 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { WorkflowRegistry } from '@/services/workflows/registry';
import { getSQLiteClient } from '@/services/database/sqlite-client';
import { WorkflowExecutor } from '@/services/agents/workflowExecutor';
import { getAutoContextSummaries } from '@/services/context/autoContext';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { workflowKey, nodeId, userContext } = body;
// Validate workflowKey
if (!workflowKey || typeof workflowKey !== 'string') {
return NextResponse.json(
{ success: false, error: 'workflowKey is required' },
{ status: 400 }
);
}
// Fetch workflow definition
const workflow = await WorkflowRegistry.getWorkflowByKey(workflowKey);
if (!workflow) {
return NextResponse.json(
{ success: false, error: `Workflow '${workflowKey}' not found` },
{ status: 404 }
);
}
if (!workflow.enabled) {
return NextResponse.json(
{ success: false, error: `Workflow '${workflowKey}' is disabled` },
{ status: 400 }
);
}
// Validate node requirement
if (workflow.requiresFocusedNode && !nodeId) {
return NextResponse.json(
{ success: false, error: `Workflow '${workflowKey}' requires a nodeId` },
{ status: 400 }
);
}
// Prevent re-running same workflow on same node within 1 hour
if (nodeId) {
const db = getSQLiteClient();
const recentRuns = db.query<{ id: number }>(
`SELECT id FROM chats
WHERE json_extract(metadata, '$.workflow_key') = ?
AND json_extract(metadata, '$.workflow_node_id') = ?
AND datetime(created_at) > datetime('now', '-1 hour')
LIMIT 1`,
[workflowKey, nodeId]
).rows;
if (recentRuns.length > 0) {
return NextResponse.json(
{
success: false,
error: `Workflow '${workflowKey}' already ran on node ${nodeId} within the last hour`
},
{ status: 429 }
);
}
}
// Build context
let contextLines: string[] = [];
if (nodeId) {
const db = getSQLiteClient();
const stmt = db.prepare('SELECT * FROM nodes WHERE id = ?');
const node = stmt.get(nodeId) as any;
if (!node) {
return NextResponse.json(
{ success: false, error: `Node ${nodeId} not found` },
{ status: 404 }
);
}
contextLines = [
`Focused Node: [NODE:${node.id}:"${node.title}"]`,
node.description ? `Description: ${node.description}` : null,
node.content ? `Content: ${node.content}` : null,
node.link ? `Link: ${node.link}` : null,
].filter(Boolean) as string[];
}
if (userContext) {
contextLines.push(`User Context: ${userContext}`);
}
// Add auto-context summaries
const autoContextSummaries = getAutoContextSummaries(6);
if (autoContextSummaries.length > 0) {
contextLines.push('Background Context (Top Hubs):');
autoContextSummaries.forEach((summary) => {
contextLines.push(
`[NODE:${summary.id}:"${summary.title}"] (${summary.edgeCount} edges)`
);
});
}
const task = `Execute workflow: ${workflow.displayName}
${workflow.instructions}
${nodeId ? `Target Node ID: ${nodeId}` : 'No specific node targeted (general workflow)'}`;
const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
try {
// Execute workflow synchronously
const result = await WorkflowExecutor.execute({
sessionId,
task,
context: contextLines,
expectedOutcome: workflow.expectedOutcome,
workflowKey,
workflowNodeId: nodeId,
});
return NextResponse.json({
success: true,
sessionId,
workflowKey,
nodeId: nodeId || null,
status: 'completed',
summary: result.summary,
message: `Workflow '${workflow.displayName}' completed`,
});
} catch (error) {
console.error('[/api/workflows/execute] Execution failed:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return NextResponse.json(
{ success: false, error: `Workflow execution failed: ${errorMessage}` },
{ status: 500 }
);
}
} catch (error) {
console.error('Error executing workflow:', error);
return NextResponse.json(
{ success: false, error: 'Failed to execute workflow' },
{ status: 500 }
);
}
}
-18
View File
@@ -1,18 +0,0 @@
import { NextResponse } from 'next/server';
import { WorkflowRegistry } from '@/services/workflows/registry';
export async function GET() {
try {
const workflows = await WorkflowRegistry.getAllWorkflows();
return NextResponse.json({
success: true,
data: workflows,
});
} catch (error) {
console.error('Error fetching workflows:', error);
return NextResponse.json(
{ success: false, error: 'Failed to fetch workflows' },
{ status: 500 }
);
}
}