feat: sync open-source context and capsule removal

- remove legacy contexts surfaces from the app, MCP bridge, standalone server, and docs
- keep getContext and retrieveQueryContext while aligning the simplified graph-only contract
- harden dev startup migration behavior and disable the accidental chat surface in open source

Generated with Codex
This commit is contained in:
“BeeRad”
2026-04-17 12:34:51 +10:00
parent 97eeb0789f
commit 07754f5030
87 changed files with 2688 additions and 4861 deletions
-22
View File
@@ -1,22 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { contextService } from '@/services/database';
export const runtime = 'nodejs';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const contextId = parseInt(id, 10);
if (Number.isNaN(contextId)) {
return NextResponse.json({ success: false, error: 'Invalid context ID' }, { status: 400 });
}
const nodes = await contextService.getNodesForContext(contextId);
return NextResponse.json({ success: true, data: nodes, count: nodes.length });
} catch (error) {
return NextResponse.json({ success: false, error: error instanceof Error ? error.message : 'Failed to fetch context nodes' }, { status: 500 });
}
}
-26
View File
@@ -1,26 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { contextService } from '@/services/database';
export const runtime = 'nodejs';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const contextId = parseInt(id, 10);
if (Number.isNaN(contextId)) {
return NextResponse.json({ success: false, error: 'Invalid context ID' }, { status: 400 });
}
const context = await contextService.getContextById(contextId);
if (!context) {
return NextResponse.json({ success: false, error: 'Context not found' }, { status: 404 });
}
return NextResponse.json({ success: true, data: context });
} catch (error) {
return NextResponse.json({ success: false, error: error instanceof Error ? error.message : 'Failed to fetch context' }, { status: 500 });
}
}
-47
View File
@@ -1,47 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { contextService } from '@/services/database';
export const runtime = 'nodejs';
export async function GET() {
try {
const contexts = await contextService.listContexts();
return NextResponse.json({ success: true, data: contexts });
} catch (error) {
return NextResponse.json({ success: false, error: error instanceof Error ? error.message : 'Failed to fetch contexts' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const context = await contextService.createContext({
name: body.name,
description: body.description,
icon: body.icon,
});
return NextResponse.json({ success: true, data: context }, { status: 201 });
} catch (error) {
return NextResponse.json({ success: false, error: error instanceof Error ? error.message : 'Failed to create context' }, { status: 400 });
}
}
export async function PUT(request: NextRequest) {
try {
const body = await request.json();
if (typeof body.id !== 'number' || !Number.isInteger(body.id) || body.id <= 0) {
return NextResponse.json({ success: false, error: 'Context id is required' }, { status: 400 });
}
const context = await contextService.updateContext({
id: body.id,
name: body.name,
description: body.description,
icon: body.icon,
});
return NextResponse.json({ success: true, data: context });
} catch (error) {
return NextResponse.json({ success: false, error: error instanceof Error ? error.message : 'Failed to update context' }, { status: 400 });
}
}
+8 -8
View File
@@ -5,8 +5,6 @@ import { chunkService } from '@/services/database/chunks';
export async function GET() {
try {
const sqlite = getSQLiteClient();
const vectorCapability = sqlite.getVectorCapability();
// Test basic database connection
const connectionTest = await sqlite.testConnection();
if (!connectionTest) {
@@ -21,7 +19,7 @@ export async function GET() {
const vectorExtensionTest = await sqlite.checkVectorExtension();
let vectorStats = null;
let chunkStats = null;
let vectorHealth = vectorCapability.available ? 'healthy' : 'unavailable';
let vectorHealth = vectorExtensionTest ? 'healthy' : 'unavailable';
try {
const totalChunks = await chunkService.getChunkCount();
@@ -32,7 +30,7 @@ export async function GET() {
coverage_percentage: null,
};
if (vectorCapability.available && vectorExtensionTest) {
if (vectorExtensionTest) {
try {
const chunksWithoutEmbeddings = await chunkService.getChunksWithoutEmbeddings();
const vectorizedCount = totalChunks - chunksWithoutEmbeddings.length;
@@ -62,9 +60,8 @@ export async function GET() {
} else {
vectorHealth = 'unavailable';
vectorStats = {
backend: vectorCapability.backend,
extension_path: vectorCapability.extensionPath,
reason: vectorCapability.available ? null : vectorCapability.reason,
extension_loaded: false,
reason: 'Vector extension unavailable in this environment.',
};
}
@@ -81,7 +78,10 @@ export async function GET() {
data: {
database_connected: connectionTest,
vector_extension_loaded: vectorExtensionTest,
vector_capability: vectorCapability,
vector_capability: {
available: vectorExtensionTest,
backend: vectorExtensionTest ? 'sqlite-vec' : 'unavailable',
},
vector_health: vectorHealth,
chunk_stats: chunkStats,
vector_stats: vectorStats,
+25 -172
View File
@@ -13,15 +13,13 @@ const SERVER_INFO = {
const instructions = [
'RA-H is a personal knowledge graph — local-first, vendor-neutral.',
'Core concepts: contexts (optional soft scopes, max 10), nodes (knowledge units), and edges (connections with explanations).',
'Core concepts: nodes (knowledge units) and edges (connections with explanations).',
'If the user is trying to find a specific existing node, use rah_search_nodes first.',
'If the user is asking a broader question or request that would benefit from graph context, use rah_retrieve_query_context.',
'Use rah_get_context only for orientation when high-level graph state would actually help.',
'Use contexts only when one obvious existing context is explicit and helpful. If unsure or if none exist, leave context empty. Do not expect automatic context assignment.',
'Search before creating: use rah_search_nodes to check if content already exists.',
'Every edge needs an explanation: why does this connection exist?',
'Never create or update an edge unless the user has explicitly confirmed the relationship.',
'Never write via rah_write_context unless the user has explicitly confirmed yes.',
].join(' ');
function getBaseUrl(request: NextRequest): string {
@@ -56,19 +54,18 @@ function createServer(request: NextRequest): McpServer {
'rah_add_node',
{
title: 'Add RA-H node',
description: 'Create a new node in the local RA-H knowledge base after you have already decided a net-new write is correct. If the user explicitly asked to save or import something and the target artifact is clear, write after duplicate/update checks. If you are only suggesting a save, propose the node first and wait for confirmation. Leave context blank by default. If the user explicitly wants context, use `context_name` rather than a numeric ID.',
description: 'Create a new node in the local RA-H knowledge base after you have already decided a net-new write is correct. If the user explicitly asked to save or import something and the target artifact is clear, write after duplicate/update checks. If you are only suggesting a save, propose the node first and wait for confirmation.',
inputSchema: {
title: z.string().min(1).max(160),
content: z.string().max(20000).optional(),
source: z.string().max(50000).optional(),
link: z.string().url().optional(),
description: z.string().max(500).optional(),
context_name: z.string().optional(),
metadata: z.record(z.any()).optional(),
chunk: z.string().max(50000).optional(),
},
},
async ({ title, content, source, link, description, context_name, metadata, chunk }) => {
async ({ title, content, source, link, description, metadata, chunk }) => {
const payload = await callRaHApi(request, '/api/nodes', {
method: 'POST',
body: JSON.stringify({
@@ -76,7 +73,6 @@ function createServer(request: NextRequest): McpServer {
source: source?.trim() || content?.trim() || chunk?.trim() || undefined,
link: link?.trim() || undefined,
description: description?.trim() || undefined,
context_name: context_name?.trim() || undefined,
metadata: metadata || {},
}),
});
@@ -97,20 +93,26 @@ function createServer(request: NextRequest): McpServer {
'rah_search_nodes',
{
title: 'Search RA-H nodes',
description: 'Find existing RA-H entries that mention a topic before adding new ones. Leave context blank by default. If the user explicitly wants a context-specific lookup, use `context_name` rather than a numeric ID. For full current-turn grounding of a substantive request, prefer `rah_retrieve_query_context`.',
description: 'Find existing RA-H entries that mention a topic before adding new ones. For full current-turn grounding of a substantive request, prefer `rah_retrieve_query_context`.',
inputSchema: {
query: z.string().min(1).max(400),
limit: z.number().min(1).max(25).optional(),
context_name: z.string().optional(),
createdAfter: z.string().optional(),
createdBefore: z.string().optional(),
eventAfter: z.string().optional(),
eventBefore: z.string().optional(),
},
},
async ({ query, limit = 10, context_name }) => {
async ({ query, limit = 10, createdAfter, createdBefore, eventAfter, eventBefore }) => {
const payload = await callRaHApi(request, '/api/nodes/direct-search', {
method: 'POST',
body: JSON.stringify({
query: query.trim(),
limit: Math.min(Math.max(limit, 1), 25),
context_name: typeof context_name === 'string' ? context_name.trim() : undefined,
createdAfter: typeof createdAfter === 'string' ? createdAfter.trim() : undefined,
createdBefore: typeof createdBefore === 'string' ? createdBefore.trim() : undefined,
eventAfter: typeof eventAfter === 'string' ? eventAfter.trim() : undefined,
eventBefore: typeof eventBefore === 'string' ? eventBefore.trim() : undefined,
}),
});
const nodes = Array.isArray(payload.data?.nodes) ? payload.data.nodes : [];
@@ -140,17 +142,15 @@ function createServer(request: NextRequest): McpServer {
inputSchema: {
query: z.string().min(1).max(1000),
focused_node_id: z.number().int().positive().nullable().optional(),
active_context_id: z.number().int().positive().nullable().optional(),
limit: z.number().min(1).max(20).optional(),
},
},
async ({ query, focused_node_id, active_context_id, limit = 6 }) => {
async ({ query, focused_node_id, limit = 6 }) => {
const payload = await callRaHApi(request, '/api/retrieval/query-context', {
method: 'POST',
body: JSON.stringify({
query,
focused_node_id: focused_node_id ?? null,
active_context_id: active_context_id ?? null,
limit,
}),
});
@@ -167,93 +167,11 @@ function createServer(request: NextRequest): McpServer {
}
);
server.registerTool(
'rah_query_contexts',
{
title: 'Query RA-H contexts',
description: 'List or inspect optional contexts. Use this only when a context is already obviously relevant or the user asks for it.',
inputSchema: {
contextId: z.number().int().positive().optional(),
name: z.string().optional(),
search: z.string().optional(),
limit: z.number().min(1).max(100).optional(),
includeNodes: z.boolean().optional(),
},
},
async ({ contextId, name, search, limit = 50, includeNodes = false }) => {
const normalizedName = typeof name === 'string' ? name.trim().toLowerCase() : '';
const normalizedSearch = typeof search === 'string' ? search.trim().toLowerCase() : '';
let contexts: any[] = [];
if (contextId) {
const payload = await callRaHApi(request, `/api/contexts/${contextId}`);
contexts = payload.data ? [payload.data] : [];
} else {
const payload = await callRaHApi(request, '/api/contexts');
contexts = Array.isArray(payload.data) ? payload.data : [];
}
if (normalizedName) {
contexts = contexts.filter((context) => context.name.toLowerCase() === normalizedName);
}
if (normalizedSearch) {
contexts = contexts.filter((context) =>
context.name.toLowerCase().includes(normalizedSearch) ||
(context.description || '').toLowerCase().includes(normalizedSearch)
);
}
contexts = contexts.slice(0, Math.min(Math.max(limit, 1), 100));
const structuredContexts = await Promise.all(
contexts.map(async (context) => {
if (!(includeNodes && contexts.length === 1)) {
return {
id: context.id,
name: context.name,
description: context.description ?? null,
icon: context.icon ?? null,
count: context.count ?? 0,
};
}
const nodesPayload = await callRaHApi(request, `/api/contexts/${context.id}/nodes`);
const nodes = Array.isArray(nodesPayload.data) ? nodesPayload.data : [];
return {
id: context.id,
name: context.name,
description: context.description ?? null,
icon: context.icon ?? null,
count: context.count ?? nodes.length,
nodes: nodes.map((node: any) => ({
id: node.id,
title: node.title,
description: node.description ?? null,
link: node.link ?? null,
context_id: node.context_id ?? null,
updated_at: node.updated_at,
})),
};
})
);
return {
content: [{ type: 'text', text: structuredContexts.length === 0 ? 'No contexts found.' : `Found ${structuredContexts.length} context(s).` }],
structuredContent: {
count: structuredContexts.length,
contexts: structuredContexts,
},
};
}
);
server.registerTool(
'rah_update_node',
{
title: 'Update RA-H node',
description: 'Update an existing node when it is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates can proceed once the target node is clear. Context is preserved by default. If the user explicitly wants to change context, use `context_name`. Use `clear_context` only when the user explicitly wants to remove the node context.',
description: 'Update an existing node when it is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates can proceed once the target node is clear.',
inputSchema: {
id: z.number().int().positive(),
updates: z.object({
@@ -262,8 +180,6 @@ function createServer(request: NextRequest): McpServer {
content: z.string().optional(),
source: z.string().optional(),
link: z.string().optional(),
context_name: z.string().optional(),
clear_context: z.boolean().optional(),
metadata: z.record(z.any()).optional(),
}),
},
@@ -283,10 +199,6 @@ function createServer(request: NextRequest): McpServer {
delete mappedUpdates.content;
delete mappedUpdates.chunk;
if (mappedUpdates.context_name && mappedUpdates.clear_context) {
throw new Error('context_name cannot be combined with clear_context: true.');
}
const payload = await callRaHApi(request, `/api/nodes/${id}`, {
method: 'PUT',
body: JSON.stringify(mappedUpdates),
@@ -571,15 +483,15 @@ function createServer(request: NextRequest): McpServer {
'rah_get_context',
{
title: 'Get RA-H context',
description: 'Get orientation context: high-level graph state, optional contexts, hub nodes, stats, and available guides.',
description: 'Get orientation context: high-level graph state, hub nodes, stats, and available guides.',
inputSchema: {},
},
async () => {
const [hubPayload, contextsPayload, guidesPayload, countPayload] = await Promise.all([
callRaHApi(request, '/api/nodes?sortBy=edges&limit=5'),
callRaHApi(request, '/api/contexts'),
const [hubPayload, guidesPayload, countPayload, edgesPayload] = await Promise.all([
callRaHApi(request, '/api/nodes?sortBy=edges&limit=10'),
callRaHApi(request, '/api/guides').catch(() => ({ data: [] })),
callRaHApi(request, '/api/nodes?limit=1').catch(() => ({ total: 0 })),
callRaHApi(request, '/api/nodes?limit=1').catch(() => ({ total: 0, count: 0 })),
callRaHApi(request, '/api/edges?limit=1').catch(() => ({ count: 0, total: 0 })),
]);
const hubNodes = Array.isArray(hubPayload.data) ? hubPayload.data.map((node: any) => ({
@@ -589,81 +501,24 @@ function createServer(request: NextRequest): McpServer {
edgeCount: node.edge_count ?? 0,
})) : [];
const contexts = Array.isArray(contextsPayload.data) ? contextsPayload.data.map((context: any) => ({
id: context.id,
name: context.name,
description: context.description ?? null,
icon: context.icon ?? null,
count: context.count ?? 0,
})) : [];
const guides = Array.isArray(guidesPayload.data) ? guidesPayload.data.map((guide: any) => guide.name) : [];
const nodeCount = countPayload.total ?? countPayload.count ?? 0;
const edgeCount = edgesPayload.total ?? edgesPayload.count ?? 0;
return {
content: [{ type: 'text', text: `Knowledge graph: ${contexts.length} optional contexts, ${hubNodes.length} hub nodes, ${guides.length} guides available.` }],
content: [{ type: 'text', text: `Knowledge graph: ${nodeCount} nodes, ${edgeCount} edges, ${guides.length} guides available.` }],
structuredContent: {
stats: {
nodeCount: countPayload.total ?? 0,
edgeCount: 0,
contextCount: contexts.length,
nodeCount,
edgeCount,
},
hubNodes,
contexts,
guides,
},
};
}
);
server.registerTool(
'rah_write_context',
{
title: 'Write RA-H context node',
description: 'Write one atomic durable context node to the graph only after the user has explicitly approved the save. Use this for agent-suggested capture after you already proposed the node briefly and got a clear yes. Prefer ordinary create/update flows for explicit user-directed capture.',
inputSchema: {
title: z.string().min(1).max(160),
description: z.string().min(1).max(500),
source: z.string().max(50000).optional(),
context_name: z.string().optional(),
metadata: z.record(z.any()).optional(),
confirmed_by_user: z.boolean(),
},
},
async ({ title, description, source, context_name, metadata, confirmed_by_user }) => {
if (!confirmed_by_user) {
throw new Error('rah_write_context requires explicit user confirmation before writing to the graph.');
}
const payload = await callRaHApi(request, '/api/nodes', {
method: 'POST',
body: JSON.stringify({
title: title.trim(),
description: description.trim(),
source: source?.trim() || undefined,
context_name: context_name?.trim() || undefined,
metadata: {
captured_by: 'human',
captured_method: 'write_context',
...(metadata || {}),
},
}),
});
const node = payload.data;
const message = payload.message || `Saved context as node #${node.id}: ${node.title}`;
return {
content: [{ type: 'text', text: message }],
structuredContent: {
success: true,
nodeId: node.id,
title: node.title,
message,
},
};
}
);
return server;
}
@@ -717,7 +572,6 @@ export async function GET(request: NextRequest) {
'rah_add_node',
'rah_search_nodes',
'rah_retrieve_query_context',
'rah_query_contexts',
'rah_update_node',
'rah_get_nodes',
'rah_create_edge',
@@ -728,7 +582,6 @@ export async function GET(request: NextRequest) {
'rah_extract_youtube',
'rah_extract_pdf',
'rah_get_context',
'rah_write_context',
];
return NextResponse.json(
+16 -32
View File
@@ -1,8 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { contextService, nodeService } from '@/services/database';
import { nodeService } from '@/services/database';
import { autoEmbedQueue } from '@/services/embedding/autoEmbedQueue';
import { hasSufficientContent } from '@/services/embedding/constants';
import { coerceDescriptionForStorage } from '@/services/database/quality';
import { applyRequestSupabaseAuth, getCurrentSupabaseToken } from '@/services/auth/internalAuth';
import { normalizeNodeLink } from '@/utils/nodeLink';
import { mergeNodeMetadata } from '@/services/nodes/metadata';
@@ -12,6 +13,7 @@ export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const cleanupAuth = applyRequestSupabaseAuth(request);
try {
const { id } = await params;
const nodeId = parseInt(id, 10);
@@ -43,6 +45,8 @@ export async function GET(
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch node'
}, { status: 500 });
} finally {
cleanupAuth();
}
}
@@ -50,6 +54,7 @@ export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const cleanupAuth = applyRequestSupabaseAuth(request);
try {
const { id } = await params;
const nodeId = parseInt(id, 10);
@@ -100,34 +105,6 @@ export async function PUT(
updates.metadata = mergeNodeMetadata(existingNode.metadata, body.metadata);
}
const hasContextName = typeof body.context_name === 'string' && body.context_name.trim().length > 0;
const wantsClearContext = body.clear_context === true;
delete updates.context_name;
delete updates.clear_context;
if (hasContextName && wantsClearContext) {
return NextResponse.json({
success: false,
error: 'context_name cannot be combined with clear_context: true.'
}, { status: 400 });
}
if (hasContextName || Object.prototype.hasOwnProperty.call(body, 'context_id') || wantsClearContext) {
try {
const resolvedContextId = await contextService.resolveContextId({
context_id: wantsClearContext ? null : body.context_id,
context_name: hasContextName ? body.context_name : undefined,
});
updates.context_id = resolvedContextId;
} catch (error) {
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Invalid context input'
}, { status: 400 });
}
}
const incomingSource = typeof body.source === 'string' ? body.source : undefined;
const existingSource = existingNode.source ?? '';
@@ -147,9 +124,11 @@ export async function PUT(
const node = await nodeService.updateNode(nodeId, updates);
if (shouldQueueEmbed) {
autoEmbedQueue.enqueue(nodeId, { reason: 'node_updated' });
}
if (shouldQueueEmbed) {
autoEmbedQueue.enqueue(nodeId, {
reason: 'node_updated',
});
}
return NextResponse.json({
success: true,
@@ -162,6 +141,8 @@ export async function PUT(
success: false,
error: error instanceof Error ? error.message : 'Failed to update node'
}, { status: 500 });
} finally {
cleanupAuth();
}
}
@@ -169,6 +150,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const cleanupAuth = applyRequestSupabaseAuth(request);
try {
const { id } = await params;
const nodeId = parseInt(id, 10);
@@ -193,5 +175,7 @@ export async function DELETE(
success: false,
error: error instanceof Error ? error.message : 'Failed to delete node'
}, { status: 500 });
} finally {
cleanupAuth();
}
}
+5 -2
View File
@@ -1,9 +1,12 @@
import { NextRequest, NextResponse } from 'next/server';
import { applyRequestSupabaseAuth } from '@/services/auth/internalAuth';
import { directNodeLookup } from '@/services/retrieval/directNodeLookup';
export const runtime = 'nodejs';
export async function POST(request: NextRequest) {
const cleanupAuth = applyRequestSupabaseAuth(request);
try {
const body = await request.json();
const query = typeof body.query === 'string' ? body.query : '';
@@ -18,8 +21,6 @@ export async function POST(request: NextRequest) {
const result = await directNodeLookup({
search: query,
limit: typeof body.limit === 'number' ? body.limit : undefined,
context_name: typeof body.context_name === 'string' ? body.context_name : undefined,
contextId: typeof body.contextId === 'number' ? body.contextId : undefined,
createdAfter: typeof body.createdAfter === 'string' ? body.createdAfter : undefined,
createdBefore: typeof body.createdBefore === 'string' ? body.createdBefore : undefined,
eventAfter: typeof body.eventAfter === 'string' ? body.eventAfter : undefined,
@@ -39,5 +40,7 @@ export async function POST(request: NextRequest) {
success: false,
error: error instanceof Error ? error.message : 'Failed to run direct node lookup',
}, { status: 500 });
} finally {
cleanupAuth();
}
}
+11 -24
View File
@@ -1,15 +1,17 @@
import { NextRequest, NextResponse } from 'next/server';
import { contextService, nodeService } from '@/services/database';
import { nodeService } from '@/services/database';
import { Node, NodeFilters } from '@/types/database';
import { autoEmbedQueue } from '@/services/embedding/autoEmbedQueue';
import { generateDescription } from '@/services/database/descriptionService';
import { coerceDescriptionForStorage } from '@/services/database/quality';
import { applyRequestSupabaseAuth, getCurrentSupabaseToken } from '@/services/auth/internalAuth';
import { normalizeNodeLink } from '@/utils/nodeLink';
import { buildCanonicalNodeMetadata } from '@/services/nodes/metadata';
export const runtime = 'nodejs';
export async function GET(request: NextRequest) {
const cleanupAuth = applyRequestSupabaseAuth(request);
try {
const searchParams = request.nextUrl.searchParams;
@@ -19,14 +21,6 @@ export async function GET(request: NextRequest) {
offset: searchParams.get('offset') ? parseInt(searchParams.get('offset')!) : 0
};
const contextIdParam = searchParams.get('contextId');
if (contextIdParam) {
const parsed = parseInt(contextIdParam, 10);
if (!Number.isNaN(parsed)) {
filters.contextId = parsed;
}
}
// Handle sortBy parameter (sortBy=edges|updated|created)
const sortByParam = searchParams.get('sortBy');
if (sortByParam === 'edges' || sortByParam === 'updated' || sortByParam === 'created' || sortByParam === 'event_date') {
@@ -57,6 +51,8 @@ export async function GET(request: NextRequest) {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch nodes'
}, { status: 500 });
} finally {
cleanupAuth();
}
}
@@ -75,6 +71,7 @@ function sanitizeTitle(title: string): string {
}
export async function POST(request: NextRequest) {
const cleanupAuth = applyRequestSupabaseAuth(request);
try {
const body = await request.json();
@@ -148,19 +145,6 @@ export async function POST(request: NextRequest) {
? body.metadata.source
: undefined;
let resolvedContextId: number | null | undefined;
try {
resolvedContextId = await contextService.resolveContextId({
context_id: body.context_id,
context_name: body.context_name,
});
} catch (error) {
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Invalid context input'
}, { status: 400 });
}
const node = await nodeService.createNode({
title: body.title,
description: finalDescription,
@@ -168,7 +152,6 @@ export async function POST(request: NextRequest) {
event_date: eventDate ?? undefined,
link: normalizedLink ?? undefined,
chunk_status: chunkStatus,
context_id: resolvedContextId,
metadata: buildCanonicalNodeMetadata({
metadata: body.metadata || {},
type: inferredType,
@@ -177,7 +160,9 @@ export async function POST(request: NextRequest) {
});
if (chunkStatus === 'not_chunked' && node.id) {
autoEmbedQueue.enqueue(node.id, { reason: 'node_created' });
autoEmbedQueue.enqueue(node.id, {
reason: 'node_created',
});
}
return NextResponse.json({
@@ -192,5 +177,7 @@ export async function POST(request: NextRequest) {
success: false,
error: error instanceof Error ? error.message : 'Failed to create node'
}, { status: 500 });
} finally {
cleanupAuth();
}
}
+5 -1
View File
@@ -1,9 +1,12 @@
import { NextRequest, NextResponse } from 'next/server';
import { applyRequestSupabaseAuth } from '@/services/auth/internalAuth';
import { retrieveQueryContext } from '@/services/retrieval/queryContext';
export const runtime = 'nodejs';
export async function POST(request: NextRequest) {
const cleanupAuth = applyRequestSupabaseAuth(request);
try {
const body = await request.json();
const query = typeof body.query === 'string' ? body.query : '';
@@ -18,7 +21,6 @@ export async function POST(request: NextRequest) {
const result = await retrieveQueryContext({
query,
focused_node_id: typeof body.focused_node_id === 'number' ? body.focused_node_id : null,
active_context_id: typeof body.active_context_id === 'number' ? body.active_context_id : null,
limit: typeof body.limit === 'number' ? body.limit : undefined,
});
@@ -31,5 +33,7 @@ export async function POST(request: NextRequest) {
success: false,
error: error instanceof Error ? error.message : 'Failed to retrieve query context',
}, { status: 500 });
} finally {
cleanupAuth();
}
}