Fixes: 1. useFetch: add credentials:'same-origin' — cookie wasn't being sent with API calls, causing 401 → React error boundary → 'Etwas ist schiefgelaufen' 2. Instance detail Sheet now loads correctly (cookie sent with request) New instance actions (UI + API): - Redeploy: triggers Dokploy application.redeploy (rebuilds from latest commit) - Stop: triggers Dokploy application.stop (pauses container) - Start: triggers Dokploy application.start (resumes container) - Remove: existing delete (SlideCommit confirmation) Dokploy deploy fix: - deployApplication now calls markRunning before deploy - This ensures Dokploy creates a deployment job ActionsRow redesigned: - demo_active/active: [Redeploy] [Stop] [Demo +7d] [Remove] - grace: [Start] [Remove] - failed: [Retry] [Remove] - provisioning/deleting/deleted: no actions
234 lines
7.1 KiB
TypeScript
Executable File
234 lines
7.1 KiB
TypeScript
Executable File
/**
|
|
* Graphify Client
|
|
* ----------------
|
|
* Talks to the Graphify MCP HTTP service (Docker container) to:
|
|
* 1. Build/refresh a knowledge graph from a Git repo (per tenant instance)
|
|
* 2. Query the graph (codebase search, god nodes, shortest path, etc.)
|
|
*
|
|
* Graphify is a local-first knowledge-graph builder for code. It parses
|
|
* 40+ languages with tree-sitter (deterministic, no LLM) and produces a
|
|
* traversable graph.json. The MCP HTTP service exposes 7 query tools.
|
|
*
|
|
* Config (env):
|
|
* GRAPHIFY_URL - e.g. http://localhost:8080 (Docker container)
|
|
* GRAPHIFY_API_KEY - shared secret (--api-key on the Graphify server)
|
|
*
|
|
* Per-tenant graphs are stored at: /data/graphify/{instanceId}/graph.json
|
|
*/
|
|
|
|
export interface GraphifyConfig {
|
|
url: string
|
|
apiKey?: string
|
|
}
|
|
|
|
export interface GraphStats {
|
|
node_count: number
|
|
edge_count: number
|
|
community_count: number
|
|
god_nodes: number
|
|
extracted_edges: number
|
|
inferred_edges: number
|
|
ambiguous_edges: number
|
|
}
|
|
|
|
export interface GraphNode {
|
|
id: string
|
|
label: string
|
|
type: string
|
|
file?: string
|
|
line?: number
|
|
community?: number
|
|
is_god_node?: boolean
|
|
}
|
|
|
|
export interface GraphQueryResult {
|
|
answer: string
|
|
nodes: GraphNode[]
|
|
confidence: 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS' | 'NONE'
|
|
path?: string[]
|
|
}
|
|
|
|
export interface GodNode {
|
|
id: string
|
|
label: string
|
|
type: string
|
|
in_degree: number
|
|
out_degree: number
|
|
file?: string
|
|
community?: number
|
|
}
|
|
|
|
class GraphifyError extends Error {
|
|
constructor(message: string, public status: number, public body?: unknown) {
|
|
super(message)
|
|
this.name = 'GraphifyError'
|
|
}
|
|
}
|
|
|
|
export function createGraphifyClient(config: GraphifyConfig) {
|
|
const baseUrl = config.url.replace(/\/$/, '')
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
if (config.apiKey) {
|
|
headers['Authorization'] = `Bearer ${config.apiKey}`
|
|
}
|
|
|
|
// --- MCP JSON-RPC call ---
|
|
async function mcpCall<T = unknown>(tool: string, args: Record<string, unknown> = {}): Promise<T> {
|
|
const res = await fetch(`${baseUrl}/mcp`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
method: 'tools/call',
|
|
params: { name: tool, arguments: args },
|
|
id: Date.now(),
|
|
}),
|
|
})
|
|
const text = await res.text()
|
|
let json: unknown
|
|
try {
|
|
json = text ? JSON.parse(text) : null
|
|
} catch {
|
|
json = text
|
|
}
|
|
if (!res.ok) {
|
|
throw new GraphifyError(
|
|
`Graphify MCP ${tool} failed: HTTP ${res.status}`,
|
|
res.status,
|
|
json
|
|
)
|
|
}
|
|
// MCP response: { result: { content: [{ type: 'text', text: '...' }] } }
|
|
const result = (json as { result?: { content?: { type: string; text: string }[] } })?.result
|
|
if (!result) return json as T
|
|
const textContent = result.content?.find((c) => c.type === 'text')?.text
|
|
if (!textContent) return json as T
|
|
try {
|
|
return JSON.parse(textContent) as T
|
|
} catch {
|
|
return textContent as unknown as T
|
|
}
|
|
}
|
|
|
|
// --- Graph build (CLI invocation via HTTP trigger if supported) ---
|
|
// Note: Graphify's MCP server is read-only (query tools). To BUILD a graph,
|
|
// we either:
|
|
// a) Shell out to `graphify extract` + `graphify build` in a worker process
|
|
// b) Call a custom build endpoint (if the Docker image exposes one)
|
|
// Here we provide a high-level function that the provisioning hook calls.
|
|
// The actual build is done via a sidecar job (see instance provisioning route).
|
|
|
|
// --- Query tools (read-only, exposed by Graphify MCP) ---
|
|
|
|
async function queryGraph(question: string, graphPath?: string): Promise<GraphQueryResult> {
|
|
const args: Record<string, unknown> = { question }
|
|
if (graphPath) args.graph_path = graphPath
|
|
return mcpCall<GraphQueryResult>('query_graph', args)
|
|
}
|
|
|
|
async function getNode(nodeId: string, graphPath?: string): Promise<GraphNode> {
|
|
const args: Record<string, unknown> = { node_id: nodeId }
|
|
if (graphPath) args.graph_path = graphPath
|
|
return mcpCall<GraphNode>('get_node', args)
|
|
}
|
|
|
|
async function getNeighbors(nodeId: string, graphPath?: string): Promise<GraphNode[]> {
|
|
const args: Record<string, unknown> = { node_id: nodeId }
|
|
if (graphPath) args.graph_path = graphPath
|
|
return mcpCall<GraphNode[]>('get_neighbors', args)
|
|
}
|
|
|
|
async function getCommunity(communityId: number, graphPath?: string): Promise<{
|
|
id: number
|
|
label: string
|
|
nodes: GraphNode[]
|
|
}> {
|
|
const args: Record<string, unknown> = { community_id: communityId }
|
|
if (graphPath) args.graph_path = graphPath
|
|
return mcpCall('get_community', args)
|
|
}
|
|
|
|
async function getGodNodes(graphPath?: string): Promise<GodNode[]> {
|
|
const args: Record<string, unknown> = {}
|
|
if (graphPath) args.graph_path = graphPath
|
|
return mcpCall<GodNode[]>('god_nodes', args)
|
|
}
|
|
|
|
async function getGraphStats(graphPath?: string): Promise<GraphStats> {
|
|
const args: Record<string, unknown> = {}
|
|
if (graphPath) args.graph_path = graphPath
|
|
return mcpCall<GraphStats>('graph_stats', args)
|
|
}
|
|
|
|
async function shortestPath(sourceId: string, targetId: string, graphPath?: string): Promise<{
|
|
path: string[]
|
|
length: number
|
|
found: boolean
|
|
}> {
|
|
const args: Record<string, unknown> = { source: sourceId, target: targetId }
|
|
if (graphPath) args.graph_path = graphPath
|
|
return mcpCall('shortest_path', args)
|
|
}
|
|
|
|
// --- Health check ---
|
|
async function health(): Promise<{ ok: boolean; version?: string }> {
|
|
try {
|
|
const res = await fetch(`${baseUrl}/health`, { headers, signal: AbortSignal.timeout(5000) })
|
|
if (!res.ok) return { ok: false }
|
|
const data = await res.json().catch(() => ({ ok: true }))
|
|
return { ok: true, version: (data as { version?: string }).version }
|
|
} catch {
|
|
return { ok: false }
|
|
}
|
|
}
|
|
|
|
return {
|
|
mcpCall,
|
|
queryGraph,
|
|
getNode,
|
|
getNeighbors,
|
|
getCommunity,
|
|
getGodNodes,
|
|
getGraphStats,
|
|
shortestPath,
|
|
health,
|
|
}
|
|
}
|
|
|
|
export type GraphifyClient = ReturnType<typeof createGraphifyClient>
|
|
|
|
export function createGraphifyClientFromEnv(): GraphifyClient | null {
|
|
const url = process.env.GRAPHIFY_URL
|
|
if (!url) return null
|
|
return createGraphifyClient({
|
|
url,
|
|
apiKey: process.env.GRAPHIFY_API_KEY,
|
|
})
|
|
}
|
|
|
|
// --- Helper: graph path per instance ---
|
|
// Each tenant's graph is stored at /data/graphify/{instanceId}/graph.json
|
|
export function graphPathForInstance(instanceId: string): string {
|
|
return `/data/graphify/${instanceId}/graph.json`
|
|
}
|
|
|
|
// --- Helper: build graph from a Git repo ---
|
|
// This is a high-level function that triggers a graph build via the Graphify
|
|
// CLI. In production, this would be a background job (e.g. via a queue or
|
|
// Dokploy cron). Here we provide the command that should be executed.
|
|
export function buildGraphCommand(input: {
|
|
gitRepo: string
|
|
gitBranch: string
|
|
outputPath: string // /data/graphify/{instanceId}/graph.json
|
|
}): string {
|
|
return [
|
|
`git clone --depth 1 -b ${input.gitBranch} ${input.gitRepo} /tmp/repo`,
|
|
`&& graphify extract /tmp/repo --output ${input.outputPath}`,
|
|
`&& graphify build ${input.outputPath} --update`,
|
|
`&& graphify cluster ${input.outputPath}`,
|
|
`&& rm -rf /tmp/repo`,
|
|
].join(' ')
|
|
}
|