Build the knowledge graph pillar with the kg package: - Node: hierarchical notes with parent/children, C2 wiki-style naming, shared tag/category pool with artifacts - Cell: content units (markdown, code, plain) with ordinal ordering - Fact: EAV tuples with transaction timestamps and retraction support - Edge: directed graph links (child, parent, related, artifact_link) Includes schema migration (002_knowledge_graph.sql), protobuf definitions (kg.proto), full gRPC KnowledgeGraphService implementation, CLI commands (node create/get), and comprehensive test coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
2.3 KiB
Go
90 lines
2.3 KiB
Go
// Command exod is the gRPC backend daemon for the exo system.
|
|
// It is the sole owner of the SQLite database and blob store.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"git.wntrmute.dev/kyle/exo/blob"
|
|
"git.wntrmute.dev/kyle/exo/config"
|
|
"git.wntrmute.dev/kyle/exo/db"
|
|
pb "git.wntrmute.dev/kyle/exo/proto/exo/v1"
|
|
"git.wntrmute.dev/kyle/exo/server"
|
|
)
|
|
|
|
var version = "dev"
|
|
|
|
func main() {
|
|
var (
|
|
listenAddr = flag.String("listen", "", "gRPC listen address (default from config)")
|
|
basePath = flag.String("base", "", "base data directory (default $HOME/exo)")
|
|
showVer = flag.Bool("version", false, "print version and exit")
|
|
)
|
|
flag.Parse()
|
|
|
|
if *showVer {
|
|
log.Printf("exod %s", version)
|
|
os.Exit(0)
|
|
}
|
|
|
|
cfg := config.Default()
|
|
if *basePath != "" {
|
|
cfg = config.FromBasePath(*basePath)
|
|
}
|
|
if *listenAddr != "" {
|
|
cfg.GRPCListenAddr = *listenAddr
|
|
}
|
|
|
|
// Ensure data directories exist.
|
|
if err := os.MkdirAll(cfg.BasePath, 0o750); err != nil {
|
|
log.Fatalf("exod: failed to create base directory: %v", err)
|
|
}
|
|
if err := os.MkdirAll(cfg.BlobStorePath, 0o750); err != nil {
|
|
log.Fatalf("exod: failed to create blob store directory: %v", err)
|
|
}
|
|
|
|
// Open and migrate the database.
|
|
database, err := db.Open(cfg.DatabasePath)
|
|
if err != nil {
|
|
log.Fatalf("exod: failed to open database: %v", err)
|
|
}
|
|
defer func() { _ = database.Close() }()
|
|
|
|
if err := db.Migrate(database); err != nil {
|
|
log.Fatalf("exod: failed to migrate database: %v", err)
|
|
}
|
|
|
|
blobStore := blob.NewStore(cfg.BlobStorePath)
|
|
|
|
// Start gRPC server.
|
|
lis, err := net.Listen("tcp", cfg.GRPCListenAddr)
|
|
if err != nil {
|
|
log.Fatalf("exod: failed to listen on %s: %v", cfg.GRPCListenAddr, err)
|
|
}
|
|
|
|
grpcServer := grpc.NewServer()
|
|
pb.RegisterArtifactServiceServer(grpcServer, server.NewArtifactServer(database, blobStore))
|
|
pb.RegisterKnowledgeGraphServiceServer(grpcServer, server.NewKGServer(database))
|
|
|
|
// Graceful shutdown on SIGINT/SIGTERM.
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
sig := <-sigCh
|
|
log.Printf("exod: received %s, shutting down", sig)
|
|
grpcServer.GracefulStop()
|
|
}()
|
|
|
|
log.Printf("exod %s listening on %s", version, cfg.GRPCListenAddr)
|
|
if err := grpcServer.Serve(lis); err != nil {
|
|
log.Fatalf("exod: gRPC server error: %v", err)
|
|
}
|
|
}
|