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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user