Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified .specify/scripts/bash/check-prerequisites.sh
100644 → 100755
Empty file.
Empty file modified .specify/scripts/bash/setup-plan.sh
100644 → 100755
Empty file.
Empty file modified .specify/scripts/bash/update-agent-context.sh
100644 → 100755
Empty file.
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,10 @@ internal/

Go rewrite of [cyborg-swarm](https://github.com/unbound-force/cyborg-swarm),
originally by [Joel Hooks](https://github.com/joelhooks).

## Active Technologies
- Go 1.25+ + `cobra` (CLI), `modernc.org/sqlite` (pure Go SQLite), stdlib `encoding/json` (MCP JSON-RPC), stdlib `os/exec` (git operations) (001-go-rewrite-phases)
- SQLite at `~/.config/swarm-tools/swarm.db` (WAL mode, compatible with cyborg-swarm) (001-go-rewrite-phases)

## Recent Changes
- 001-go-rewrite-phases: Added Go 1.25+ + `cobra` (CLI), `modernc.org/sqlite` (pure Go SQLite), stdlib `encoding/json` (MCP JSON-RPC), stdlib `os/exec` (git operations)
58 changes: 58 additions & 0 deletions cmd/replicator/doctor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package main

import (
"fmt"
"time"

"github.com/unbound-force/replicator/internal/config"
"github.com/unbound-force/replicator/internal/db"
"github.com/unbound-force/replicator/internal/doctor"
)

// runDoctor executes health checks and prints results as a table.
func runDoctor(cfg *config.Config) error {
store, err := db.Open(cfg.DatabasePath)
if err != nil {
return fmt.Errorf("open database: %w", err)
}
defer store.Close()

results, err := doctor.Run(store, cfg)
if err != nil {
return fmt.Errorf("run checks: %w", err)
}

// Print header.
fmt.Printf("%-12s %-6s %s\n", "CHECK", "STATUS", "MESSAGE")
fmt.Printf("%-12s %-6s %s\n", "-----", "------", "-------")

hasFailure := false
for _, r := range results {
icon := statusIcon(r.Status)
fmt.Printf("%-12s %s %-4s %s (%s)\n", r.Name, icon, r.Status, r.Message, r.Duration.Round(time.Millisecond))
if r.Status == "fail" {
hasFailure = true
}
}

if hasFailure {
fmt.Println("\nSome checks failed. Run 'replicator setup' to fix common issues.")
} else {
fmt.Println("\nAll checks passed.")
}

return nil
}

func statusIcon(status string) string {
switch status {
case "pass":
return "\u2713" // checkmark
case "fail":
return "\u2717" // X mark
case "warn":
return "!" // warning
default:
return "?"
}
}
60 changes: 60 additions & 0 deletions cmd/replicator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ via Dewey).`,
root.AddCommand(serveCmd())
root.AddCommand(cellsCmd())
root.AddCommand(versionCmd())
root.AddCommand(doctorCmd())
root.AddCommand(statsCmd())
root.AddCommand(queryCmd())
root.AddCommand(setupCmd())

if err := root.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down Expand Up @@ -73,3 +77,59 @@ func versionCmd() *cobra.Command {
},
}
}

func doctorCmd() *cobra.Command {
return &cobra.Command{
Use: "doctor",
Short: "Run health checks on the replicator environment",
Long: "Checks git, database, Dewey connectivity, and config directory.",
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
return runDoctor(cfg)
},
}
}

func statsCmd() *cobra.Command {
return &cobra.Command{
Use: "stats",
Short: "Show database statistics",
Long: "Displays event counts, recent activity, and cell status summary.",
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Load()
return runStats(cfg)
},
}
}

func queryCmd() *cobra.Command {
var listFlag bool

cmd := &cobra.Command{
Use: "query [preset]",
Short: "Run a preset database query",
Long: "Executes a named preset query against the database. Use --list to see available presets.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if listFlag || len(args) == 0 {
listQueryPresets()
return nil
}
cfg := config.Load()
return runQuery(cfg, args[0])
},
}
cmd.Flags().BoolVar(&listFlag, "list", false, "List available query presets")
return cmd
}

func setupCmd() *cobra.Command {
return &cobra.Command{
Use: "setup",
Short: "Initialize replicator environment",
Long: "Creates config directory, initializes database, and verifies git.",
RunE: func(cmd *cobra.Command, args []string) error {
return runSetup()
},
}
}
33 changes: 33 additions & 0 deletions cmd/replicator/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"fmt"
"os"
"strings"

"github.com/unbound-force/replicator/internal/config"
"github.com/unbound-force/replicator/internal/db"
"github.com/unbound-force/replicator/internal/query"
)

// runQuery executes a preset query and prints results.
func runQuery(cfg *config.Config, presetName string) error {
store, err := db.Open(cfg.DatabasePath)
if err != nil {
return fmt.Errorf("open database: %w", err)
}
defer store.Close()

return query.Run(store, presetName, os.Stdout)
}

// listQueryPresets prints available preset names.
func listQueryPresets() {
fmt.Println("Available query presets:")
for _, p := range query.ListPresets() {
fmt.Printf(" %s\n", p)
}
fmt.Println()
fmt.Println("Usage: replicator query <preset>")
fmt.Printf("Example: replicator query %s\n", strings.Join(query.ListPresets()[:1], ""))
}
12 changes: 11 additions & 1 deletion cmd/replicator/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import (
"github.com/unbound-force/replicator/internal/config"
"github.com/unbound-force/replicator/internal/db"
"github.com/unbound-force/replicator/internal/mcp"
"github.com/unbound-force/replicator/internal/memory"
"github.com/unbound-force/replicator/internal/tools/hive"
memorytools "github.com/unbound-force/replicator/internal/tools/memory"
"github.com/unbound-force/replicator/internal/tools/registry"
swarmtools "github.com/unbound-force/replicator/internal/tools/swarm"
swarmmailtools "github.com/unbound-force/replicator/internal/tools/swarmmail"
)

// serveMCP starts the MCP JSON-RPC server on stdio.
Expand All @@ -20,7 +24,13 @@ func serveMCP() error {

reg := registry.New()
hive.Register(reg, store)
swarmmailtools.Register(reg, store)
swarmtools.Register(reg, store)

server := mcp.NewServer(reg)
// Memory tools proxy to Dewey for semantic search.
memClient := memory.NewClient(cfg.DeweyURL)
memorytools.Register(reg, memClient)

server := mcp.NewServer(reg, Version)
return server.ServeStdio()
}
51 changes: 51 additions & 0 deletions cmd/replicator/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/unbound-force/replicator/internal/config"
"github.com/unbound-force/replicator/internal/db"
)

// runSetup creates the config directory, initializes the database, and
// verifies git is available.
func runSetup() error {
// 1. Create config directory.
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("determine home directory: %w", err)
}

configDir := filepath.Join(home, ".config", "swarm-tools")
if err := os.MkdirAll(configDir, 0o755); err != nil {
return fmt.Errorf("create config directory: %w", err)
}
fmt.Printf("\u2713 Config directory: %s\n", configDir)

// 2. Initialize database.
cfg := config.Load()
store, err := db.Open(cfg.DatabasePath)
if err != nil {
return fmt.Errorf("initialize database: %w", err)
}
store.Close()
fmt.Printf("\u2713 Database: %s\n", cfg.DatabasePath)

// 3. Verify git.
cmd := exec.Command("git", "--version")
out, err := cmd.Output()
if err != nil {
fmt.Printf("\u2717 Git: not found (%v)\n", err)
fmt.Println(" Install git: https://git-scm.com/downloads")
} else {
fmt.Printf("\u2713 Git: %s\n", strings.TrimSpace(string(out)))
}

fmt.Println()
fmt.Println("Setup complete. Run 'replicator doctor' to verify all checks pass.")
return nil
}
21 changes: 21 additions & 0 deletions cmd/replicator/stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"fmt"
"os"

"github.com/unbound-force/replicator/internal/config"
"github.com/unbound-force/replicator/internal/db"
"github.com/unbound-force/replicator/internal/stats"
)

// runStats queries the database and prints statistics.
func runStats(cfg *config.Config) error {
store, err := db.Open(cfg.DatabasePath)
if err != nil {
return fmt.Errorf("open database: %w", err)
}
defer store.Close()

return stats.Run(store, os.Stdout)
}
3 changes: 3 additions & 0 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,7 @@ var migrations = []string{
migrationAgents,
migrationCells,
migrationCellEvents,
migrationSessions,
migrationMessages,
migrationReservations,
}
2 changes: 1 addition & 1 deletion internal/db/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ func TestOpenMemory(t *testing.T) {
defer store.Close()

// Verify tables exist.
tables := []string{"events", "agents", "beads", "cell_events"}
tables := []string{"events", "agents", "beads", "cell_events", "sessions", "messages", "reservations"}
for _, table := range tables {
var name string
err := store.DB.QueryRow(
Expand Down
47 changes: 47 additions & 0 deletions internal/db/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,50 @@ CREATE TABLE IF NOT EXISTS cell_events (

CREATE INDEX IF NOT EXISTS idx_cell_events_cell ON cell_events(cell_id);
`

const migrationSessions = `
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
agent_name TEXT,
project_path TEXT,
started_at TEXT,
ended_at TEXT,
handoff_notes TEXT,
active_cell_id TEXT
);
`

const migrationMessages = `
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_agent TEXT,
to_agents TEXT,
subject TEXT,
body TEXT,
importance TEXT DEFAULT 'normal',
thread_id TEXT,
ack_required INTEGER DEFAULT 0,
acknowledged INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_messages_from_agent ON messages(from_agent);
CREATE INDEX IF NOT EXISTS idx_messages_created ON messages(created_at);
`

const migrationReservations = `
CREATE TABLE IF NOT EXISTS reservations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_name TEXT,
project_path TEXT,
path TEXT,
exclusive INTEGER DEFAULT 1,
reason TEXT,
ttl_seconds INTEGER DEFAULT 300,
created_at TEXT DEFAULT (datetime('now')),
expires_at TEXT
);

CREATE INDEX IF NOT EXISTS idx_reservations_path ON reservations(path);
CREATE INDEX IF NOT EXISTS idx_reservations_agent ON reservations(agent_name);
`
Loading
Loading