mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
update
This commit is contained in:
+185
-27
@@ -1,25 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/yourorg/devour/internal/projectstate"
|
||||
"github.com/yourorg/devour/internal/scraper"
|
||||
"github.com/yourorg/devour/internal/search"
|
||||
"github.com/yourorg/devour/internal/server"
|
||||
)
|
||||
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Start the MCP server",
|
||||
Long: `Start the Devour MCP server.
|
||||
Short: "Start the local Devour RPC server",
|
||||
Long: `Start the Devour RPC server.
|
||||
|
||||
In local mode (default), the server communicates via stdio, making it
|
||||
suitable for use as an OpenCode skill.
|
||||
|
||||
In remote mode (--remote flag), the server listens on HTTP and exposes
|
||||
a REST API for multi-user access.
|
||||
Local mode (default): JSON-RPC over stdin/stdout for agent/skill integration.
|
||||
Remote mode (--remote): experimental HTTP RPC endpoint at /rpc.
|
||||
|
||||
Examples:
|
||||
devour serve # Local mode (stdio)
|
||||
devour serve --remote # Remote mode on default port
|
||||
devour serve
|
||||
devour serve --remote
|
||||
devour serve --remote --port 3000`,
|
||||
RunE: runServe,
|
||||
}
|
||||
@@ -31,31 +35,185 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
serveCmd.Flags().BoolVar(&serveRemote, "remote", false, "run as remote HTTP server")
|
||||
serveCmd.Flags().BoolVar(&serveRemote, "remote", false, "run as remote HTTP server (experimental)")
|
||||
serveCmd.Flags().IntVarP(&servePort, "port", "p", 8080, "HTTP port (remote mode only)")
|
||||
serveCmd.Flags().StringVar(&serveHost, "host", "localhost", "HTTP host (remote mode only)")
|
||||
}
|
||||
|
||||
func runServe(cmd *cobra.Command, args []string) error {
|
||||
if serveRemote {
|
||||
fmt.Printf("🚀 Starting Devour server in remote mode\n")
|
||||
fmt.Printf(" Host: %s\n", serveHost)
|
||||
fmt.Printf(" Port: %d\n", servePort)
|
||||
fmt.Printf(" URL: http://%s:%d\n", serveHost, servePort)
|
||||
|
||||
// TODO: Start HTTP MCP server
|
||||
return fmt.Errorf("remote mode not yet implemented")
|
||||
if _, err := loadAppConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("🚀 Starting Devour server in local mode (stdio)")
|
||||
fmt.Println(" Communicating via JSON-RPC over stdin/stdout")
|
||||
srvCfg := &server.Config{
|
||||
Mode: "local",
|
||||
Transport: "stdio",
|
||||
Host: serveHost,
|
||||
Port: servePort,
|
||||
Handler: func(ctx context.Context, method string, params json.RawMessage) (any, error) {
|
||||
return handleServeMethod(ctx, method, params)
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Start stdio MCP server
|
||||
// Should handle JSON-RPC messages for:
|
||||
// - devour_query
|
||||
// - devour_add
|
||||
// - devour_status
|
||||
// - devour_sync
|
||||
if serveRemote {
|
||||
srvCfg.Mode = "remote"
|
||||
fmt.Printf("🚀 Starting Devour RPC server in remote experimental mode\n")
|
||||
fmt.Printf(" URL: http://%s:%d/rpc\n", serveHost, servePort)
|
||||
} else {
|
||||
fmt.Println("🚀 Starting Devour RPC server in local mode (stdio)")
|
||||
fmt.Println(" Protocol: JSON-RPC 2.0 over stdin/stdout")
|
||||
}
|
||||
|
||||
return fmt.Errorf("local mode not yet implemented")
|
||||
srv := server.NewServer(srvCfg)
|
||||
return srv.Start(context.Background())
|
||||
}
|
||||
|
||||
func handleServeMethod(ctx context.Context, method string, params json.RawMessage) (any, error) {
|
||||
// The method implementation needs full typed config. Load per-call to avoid stale state.
|
||||
loadedCfg, err := loadAppConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(method) {
|
||||
case "devour_query":
|
||||
var req struct {
|
||||
Query string `json:"query"`
|
||||
Limit int `json:"limit"`
|
||||
Threshold float64 `json:"threshold"`
|
||||
}
|
||||
if len(params) > 0 {
|
||||
_ = json.Unmarshal(params, &req)
|
||||
}
|
||||
engine := search.NewEngine(loadedCfg)
|
||||
results, stats, err := engine.Search(ctx, req.Query, search.SearchOptions{Limit: req.Limit, Threshold: req.Threshold})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"query": req.Query, "count": len(results), "results": results, "indexed": stats.Documents}, nil
|
||||
|
||||
case "devour_status":
|
||||
docsStats, err := projectstate.CollectDocsStats(loadedCfg.Storage.DocsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state, _ := projectstate.LoadSourceState(loadedCfg.Storage.MetadataDir)
|
||||
engine := search.NewEngine(loadedCfg)
|
||||
idxStats, _ := engine.EnsureIndexed(ctx)
|
||||
return map[string]any{
|
||||
"documents": docsStats.DocumentCount,
|
||||
"storage_bytes": docsStats.StorageBytes,
|
||||
"last_updated": docsStats.LastUpdated,
|
||||
"sources": state.Sources,
|
||||
"indexed_docs": idxStats.Documents,
|
||||
"index_timestamp": idxStats.LastIndexedAt,
|
||||
}, nil
|
||||
|
||||
case "devour_scrape":
|
||||
var req struct {
|
||||
Source string `json:"source"`
|
||||
Type string `json:"type"`
|
||||
Format string `json:"format"`
|
||||
Output string `json:"output"`
|
||||
Query string `json:"query"`
|
||||
ResultLimit int `json:"result_limit"`
|
||||
Domains []string `json:"domains"`
|
||||
Include []string `json:"include"`
|
||||
Exclude []string `json:"exclude"`
|
||||
}
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(req.Source) == "" {
|
||||
return nil, fmt.Errorf("source is required")
|
||||
}
|
||||
st := scraper.SourceType(req.Type)
|
||||
if st == "" {
|
||||
st = detectSourceType(req.Source)
|
||||
}
|
||||
source := &scraper.Source{
|
||||
Name: extractName(req.Source),
|
||||
Type: st,
|
||||
URL: req.Source,
|
||||
Query: strings.TrimSpace(req.Query),
|
||||
ResultLimit: req.ResultLimit,
|
||||
Domains: append([]string(nil), req.Domains...),
|
||||
Include: append([]string(nil), req.Include...),
|
||||
Exclude: append([]string(nil), req.Exclude...),
|
||||
}
|
||||
if st == scraper.SourceTypeLocal {
|
||||
source.Path = req.Source
|
||||
}
|
||||
applySourceProfile(source)
|
||||
prevFormat := scrapeFormat
|
||||
prevOutput := scrapeOutput
|
||||
prevAllowEmpty := scrapeAllowEmpty
|
||||
scrapeFormat = coalesceString(req.Format, "json")
|
||||
scrapeOutput = req.Output
|
||||
scrapeAllowEmpty = false
|
||||
count, err := scrapeOne(nil, loadedCfg, source, resolveOutputDir(loadedCfg, req.Output))
|
||||
scrapeFormat = prevFormat
|
||||
scrapeOutput = prevOutput
|
||||
scrapeAllowEmpty = prevAllowEmpty
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"source": req.Source, "type": st, "documents": count}, nil
|
||||
|
||||
case "devour_ask":
|
||||
var req struct {
|
||||
Question string `json:"question"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(req.Question) == "" {
|
||||
return nil, fmt.Errorf("question is required")
|
||||
}
|
||||
limit := req.Limit
|
||||
if limit <= 0 {
|
||||
limit = 5
|
||||
}
|
||||
engine := search.NewEngine(loadedCfg)
|
||||
results, _, err := engine.Search(ctx, req.Question, search.SearchOptions{Limit: limit})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
summary := "No relevant docs found."
|
||||
if len(results) > 0 {
|
||||
summary = results[0].Snippet
|
||||
}
|
||||
return map[string]any{"question": req.Question, "summary": summary, "sources": results}, nil
|
||||
|
||||
case "devour_sync":
|
||||
prevForce, prevRebuild, prevSource := syncForce, syncRebuild, syncSource
|
||||
var req struct {
|
||||
Source string `json:"source"`
|
||||
Force bool `json:"force"`
|
||||
Rebuild bool `json:"rebuild"`
|
||||
}
|
||||
if len(params) > 0 {
|
||||
_ = json.Unmarshal(params, &req)
|
||||
}
|
||||
syncForce = req.Force
|
||||
syncRebuild = req.Rebuild
|
||||
syncSource = req.Source
|
||||
err := runSync(nil, nil)
|
||||
syncForce, syncRebuild, syncSource = prevForce, prevRebuild, prevSource
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"ok": true}, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown method: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
func coalesceString(primary, fallback string) string {
|
||||
if strings.TrimSpace(primary) != "" {
|
||||
return primary
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user