Files
AOS/src/lib/graphify.ts
T
Z User 32a8b01845 fix: instance detail crash + add play/stop/redeploy/remove actions
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
2026-10-29 16:29:28 +00:00

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(' ')
}