-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathmain.go
More file actions
174 lines (157 loc) · 5.99 KB
/
main.go
File metadata and controls
174 lines (157 loc) · 5.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/skridlevsky/graphthulhu/backend"
"github.com/skridlevsky/graphthulhu/client"
"github.com/skridlevsky/graphthulhu/vault"
)
var version = "dev"
func main() {
// Handle top-level help and version flags.
if len(os.Args) >= 2 {
switch os.Args[1] {
case "-h", "-help", "--help", "help":
printUsage()
return
case "-v", "-version", "--version":
fmt.Println(version)
return
}
}
// Route to subcommand if first arg is a known command (not a flag).
if len(os.Args) >= 2 && !strings.HasPrefix(os.Args[1], "-") {
c := client.New("", "")
switch os.Args[1] {
case "serve":
runServe(os.Args[2:])
case "journal":
runJournal(os.Args[2:], c)
case "add":
runAdd(os.Args[2:], c)
case "search":
runSearch(os.Args[2:], c)
case "version":
fmt.Println(version)
default:
fmt.Fprintf(os.Stderr, "graphthulhu: unknown command %q\n\n", os.Args[1])
printUsage()
os.Exit(1)
}
return
}
// Default: MCP server (backward compatible).
runServe(os.Args[1:])
}
func runServe(args []string) {
fs := flag.NewFlagSet("serve", flag.ExitOnError)
readOnly := fs.Bool("read-only", false, "Disable all write operations")
backendType := fs.String("backend", "", "Backend type: logseq (default) or obsidian")
vaultPath := fs.String("vault", "", "Path to Obsidian vault (required for obsidian backend)")
dailyFolder := fs.String("daily-folder", "daily notes", "Daily notes subfolder name (obsidian only)")
includeHidden := fs.Bool("include-hidden", false, "Index directories starting with '.' (obsidian only, .git is always skipped)")
httpAddr := fs.String("http", "", "HTTP address to listen on (e.g. :8080). Uses streamable HTTP transport instead of stdio.")
fs.Parse(args)
// Resolve backend from flag or environment.
bt := *backendType
if bt == "" {
bt = os.Getenv("GRAPHTHULHU_BACKEND")
}
if bt == "" {
bt = "logseq"
}
var b backend.Backend
switch bt {
case "obsidian":
vp := *vaultPath
if vp == "" {
vp = os.Getenv("OBSIDIAN_VAULT_PATH")
}
if vp == "" {
fmt.Fprintf(os.Stderr, "graphthulhu: --vault or OBSIDIAN_VAULT_PATH required for obsidian backend\n")
os.Exit(1)
}
vc := vault.New(vp, vault.WithDailyFolder(*dailyFolder), vault.WithIncludeHidden(*includeHidden))
if err := vc.Load(); err != nil {
fmt.Fprintf(os.Stderr, "graphthulhu: failed to load vault: %v\n", err)
os.Exit(1)
}
vc.BuildBacklinks()
// Start file watcher.
if err := vc.Watch(); err != nil {
fmt.Fprintf(os.Stderr, "graphthulhu: failed to start watcher: %v\n", err)
os.Exit(1)
}
defer vc.Close()
b = vc
case "logseq":
lsClient := client.New("", "")
checkGraphVersionControl(lsClient)
b = lsClient
default:
fmt.Fprintf(os.Stderr, "graphthulhu: unknown backend %q (use logseq or obsidian)\n", bt)
os.Exit(1)
}
srv := newServer(b, *readOnly)
if *httpAddr != "" {
// Streamable HTTP transport — serves multiple clients.
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
return srv
}, nil)
fmt.Fprintf(os.Stderr, "graphthulhu: listening on %s\n", *httpAddr)
if err := http.ListenAndServe(*httpAddr, handler); err != nil {
fmt.Fprintf(os.Stderr, "graphthulhu: %v\n", err)
os.Exit(1)
}
} else {
// Default: stdio transport for MCP client integration.
if err := srv.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
fmt.Fprintf(os.Stderr, "graphthulhu: %v\n", err)
os.Exit(1)
}
}
}
func printUsage() {
fmt.Fprintf(os.Stderr, "graphthulhu %s — Knowledge graph MCP server & CLI\n\n", version)
fmt.Fprintf(os.Stderr, "Usage:\n")
fmt.Fprintf(os.Stderr, " graphthulhu Start MCP server (default, Logseq)\n")
fmt.Fprintf(os.Stderr, " graphthulhu serve [flags] Start MCP server\n")
fmt.Fprintf(os.Stderr, " graphthulhu journal [flags] TEXT Append block to today's journal\n")
fmt.Fprintf(os.Stderr, " graphthulhu add -p PAGE TEXT Append block to a page\n")
fmt.Fprintf(os.Stderr, " graphthulhu search QUERY Full-text search across the graph\n")
fmt.Fprintf(os.Stderr, " graphthulhu version Print version\n")
fmt.Fprintf(os.Stderr, "\nServe flags:\n")
fmt.Fprintf(os.Stderr, " --backend logseq|obsidian Backend type (default: logseq)\n")
fmt.Fprintf(os.Stderr, " --vault PATH Obsidian vault path\n")
fmt.Fprintf(os.Stderr, " --daily-folder NAME Daily notes folder (default: daily notes)\n")
fmt.Fprintf(os.Stderr, " --include-hidden Index directories starting with '.' (obsidian only)\n")
fmt.Fprintf(os.Stderr, " --read-only Disable write operations\n")
fmt.Fprintf(os.Stderr, " --http ADDR Listen on HTTP (e.g. :8080) instead of stdio\n")
fmt.Fprintf(os.Stderr, "\nAll CLI commands read from stdin when no TEXT argument is given.\n")
fmt.Fprintf(os.Stderr, "Environment: LOGSEQ_API_URL (default http://127.0.0.1:12315)\n")
fmt.Fprintf(os.Stderr, " LOGSEQ_API_TOKEN\n")
fmt.Fprintf(os.Stderr, " GRAPHTHULHU_BACKEND Backend type\n")
fmt.Fprintf(os.Stderr, " OBSIDIAN_VAULT_PATH Obsidian vault path\n")
}
// checkGraphVersionControl warns on stderr if the Logseq graph is not git-controlled.
// Best-effort: silently skips if Logseq is not running or the API is unreachable.
func checkGraphVersionControl(c *client.Client) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
graph, err := c.GetCurrentGraph(ctx)
if err != nil || graph == nil || graph.Path == "" {
return
}
gitDir := filepath.Join(graph.Path, ".git")
if _, err := os.Stat(gitDir); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "graphthulhu: WARNING: graph %q at %s is not version controlled\n", graph.Name, graph.Path)
fmt.Fprintf(os.Stderr, "graphthulhu: Write operations cannot be undone. Consider: cd %s && git init && git add -A && git commit -m 'initial'\n", graph.Path)
}
}