summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/root.go1
-rw-r--r--internal/cli/serve.go88
2 files changed, 86 insertions, 3 deletions
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 7c4f2ff..5c6184e 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -60,6 +60,7 @@ func NewRootCmd() *cobra.Command {
}
cfg.DBPath = filepath.Join(cfg.DataDir, "claudomator.db")
cfg.LogDir = filepath.Join(cfg.DataDir, "executions")
+ cfg.DropsDir = filepath.Join(cfg.DataDir, "drops")
return nil
}
diff --git a/internal/cli/serve.go b/internal/cli/serve.go
index 94f0c5d..1753a64 100644
--- a/internal/cli/serve.go
+++ b/internal/cli/serve.go
@@ -1,6 +1,7 @@
package cli
import (
+ "bytes"
"context"
"fmt"
"net/http"
@@ -10,6 +11,7 @@ import (
"syscall"
"time"
+ "github.com/BurntSushi/toml"
"github.com/thepeterstone/claudomator/internal/api"
"github.com/thepeterstone/claudomator/internal/executor"
"github.com/thepeterstone/claudomator/internal/notify"
@@ -44,6 +46,23 @@ func serve(addr string) error {
return fmt.Errorf("creating dirs: %w", err)
}
+ // Auto-generate VAPID keys if not configured.
+ if cfg.VAPIDPublicKey == "" || cfg.VAPIDPrivateKey == "" {
+ pub, priv, err := notify.GenerateVAPIDKeys()
+ if err != nil {
+ return fmt.Errorf("generating VAPID keys: %w", err)
+ }
+ cfg.VAPIDPublicKey = pub
+ cfg.VAPIDPrivateKey = priv
+ // Write new keys back to config file.
+ if cfgFile != "" {
+ if err := saveVAPIDToConfig(cfgFile, pub, priv); err != nil {
+ // Non-fatal: log but continue.
+ fmt.Fprintf(os.Stderr, "warning: failed to persist VAPID keys to %s: %v\n", cfgFile, err)
+ }
+ }
+ }
+
store, err := storage.Open(cfg.DBPath)
if err != nil {
return fmt.Errorf("opening db: %w", err)
@@ -56,22 +75,24 @@ func serve(addr string) error {
if len(addr) > 0 && addr[0] != ':' {
apiURL = "http://" + addr
}
-
+
runners := map[string]executor.Runner{
"claude": &executor.ClaudeRunner{
BinaryPath: cfg.ClaudeBinaryPath,
Logger: logger,
LogDir: cfg.LogDir,
APIURL: apiURL,
+ DropsDir: cfg.DropsDir,
},
"gemini": &executor.GeminiRunner{
BinaryPath: cfg.GeminiBinaryPath,
Logger: logger,
LogDir: cfg.LogDir,
APIURL: apiURL,
+ DropsDir: cfg.DropsDir,
},
}
-
+
pool := executor.NewPool(cfg.MaxConcurrent, runners, store, logger)
if cfg.GeminiBinaryPath != "" {
pool.Classifier = &executor.Classifier{GeminiBinaryPath: cfg.GeminiBinaryPath}
@@ -81,9 +102,26 @@ func serve(addr string) error {
pool.RecoverStaleBlocked()
srv := api.NewServer(store, pool, logger, cfg.ClaudeBinaryPath, cfg.GeminiBinaryPath)
+
+ // Configure notifiers: combine webhook (if set) with web push.
+ notifiers := []notify.Notifier{}
if cfg.WebhookURL != "" {
- srv.SetNotifier(notify.NewWebhookNotifier(cfg.WebhookURL, logger))
+ notifiers = append(notifiers, notify.NewWebhookNotifier(cfg.WebhookURL, logger))
+ }
+ webPushNotifier := &notify.WebPushNotifier{
+ Store: store,
+ VAPIDPublicKey: cfg.VAPIDPublicKey,
+ VAPIDPrivateKey: cfg.VAPIDPrivateKey,
+ VAPIDEmail: cfg.VAPIDEmail,
+ Logger: logger,
}
+ notifiers = append(notifiers, webPushNotifier)
+ srv.SetNotifier(notify.NewMultiNotifier(logger, notifiers...))
+
+ srv.SetVAPIDConfig(cfg.VAPIDPublicKey, cfg.VAPIDPrivateKey, cfg.VAPIDEmail)
+ srv.SetPushStore(store)
+ srv.SetDropsDir(cfg.DropsDir)
+
if cfg.WorkspaceRoot != "" {
srv.SetWorkspaceRoot(cfg.WorkspaceRoot)
}
@@ -125,3 +163,47 @@ func serve(addr string) error {
}
return nil
}
+
+// saveVAPIDToConfig appends VAPID key assignments to the config file.
+// It reads the existing file (if any), then writes a complete TOML file with
+// the new keys merged in. Uses toml encoder for correctness.
+func saveVAPIDToConfig(path, pub, priv string) error {
+ existing := cfg // already loaded
+
+ // Marshal the full config back including the new VAPID keys.
+ // We use a struct alias to only encode fields we want persisted.
+ type persistedConfig struct {
+ DataDir string `toml:"data_dir,omitempty"`
+ ClaudeBinaryPath string `toml:"claude_binary_path,omitempty"`
+ GeminiBinaryPath string `toml:"gemini_binary_path,omitempty"`
+ MaxConcurrent int `toml:"max_concurrent,omitempty"`
+ DefaultTimeout string `toml:"default_timeout,omitempty"`
+ ServerAddr string `toml:"server_addr,omitempty"`
+ WebhookURL string `toml:"webhook_url,omitempty"`
+ WorkspaceRoot string `toml:"workspace_root,omitempty"`
+ WebhookSecret string `toml:"webhook_secret,omitempty"`
+ VAPIDPublicKey string `toml:"vapid_public_key,omitempty"`
+ VAPIDPrivateKey string `toml:"vapid_private_key,omitempty"`
+ VAPIDEmail string `toml:"vapid_email,omitempty"`
+ }
+ pc := persistedConfig{
+ DataDir: existing.DataDir,
+ ClaudeBinaryPath: existing.ClaudeBinaryPath,
+ GeminiBinaryPath: existing.GeminiBinaryPath,
+ MaxConcurrent: existing.MaxConcurrent,
+ DefaultTimeout: existing.DefaultTimeout,
+ ServerAddr: existing.ServerAddr,
+ WebhookURL: existing.WebhookURL,
+ WorkspaceRoot: existing.WorkspaceRoot,
+ WebhookSecret: existing.WebhookSecret,
+ VAPIDPublicKey: pub,
+ VAPIDPrivateKey: priv,
+ VAPIDEmail: existing.VAPIDEmail,
+ }
+
+ var buf bytes.Buffer
+ if err := toml.NewEncoder(&buf).Encode(pc); err != nil {
+ return fmt.Errorf("encoding config: %w", err)
+ }
+ return os.WriteFile(path, buf.Bytes(), 0600)
+}