mirror of
https://github.com/tiennm99/claude-code-routine-trigger.git
synced 2026-10-14 06:17:37 +00:00
feat: implement core daemon (config, fire client, scheduler)
This commit is contained in:
1 parent
981ec3255e
commit
9c4d7a532f
6 files changed
+384
-1
No files matched your search
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
headerVersion = "2023-06-01"
|
||||
headerBeta = "experimental-cc-routine-2026-04-01"
|
||||
)
|
||||
|
||||
// FireClient POSTs to the Anthropic /fire endpoint with the headers and body
|
||||
// shape required by the Claude Code routines beta. Each call creates a new
|
||||
// session; the daemon never retries on failure.
|
||||
type FireClient struct {
|
||||
URL string
|
||||
Token string
|
||||
HTTP *http.Client
|
||||
Template *template.Template
|
||||
TZ *time.Location
|
||||
Log *slog.Logger
|
||||
NowFunc func() time.Time // overridable for tests; defaults to time.Now
|
||||
}
|
||||
|
||||
type fireRequest struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type fireResponse struct {
|
||||
Type string `json:"type"`
|
||||
ClaudeCodeSessionID string `json:"claude_code_session_id"`
|
||||
ClaudeCodeSessionURL string `json:"claude_code_session_url"`
|
||||
}
|
||||
|
||||
// Fire renders the text template, POSTs to the routine /fire endpoint, and
|
||||
// logs the outcome. Returns nil on transport-level success regardless of HTTP
|
||||
// status — non-2xx is logged at error level so the daemon stays up.
|
||||
// Returns a non-nil error only for unrecoverable client-side problems
|
||||
// (template render failure or request build failure).
|
||||
func (c *FireClient) Fire(ctx context.Context, cronExpr string) error {
|
||||
now := c.now()
|
||||
text, err := c.renderText(now, cronExpr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("render text: %w", err)
|
||||
}
|
||||
|
||||
body, err := json.Marshal(fireRequest{Text: text})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.URL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
req.Header.Set("anthropic-version", headerVersion)
|
||||
req.Header.Set("anthropic-beta", headerBeta)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
c.Log.Error("fire request failed", "cron", cronExpr, "err", err.Error())
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
c.Log.Error("fire non-2xx response",
|
||||
"cron", cronExpr,
|
||||
"status", resp.StatusCode,
|
||||
"body", string(respBody))
|
||||
return nil
|
||||
}
|
||||
|
||||
var fr fireResponse
|
||||
if err := json.Unmarshal(respBody, &fr); err != nil {
|
||||
c.Log.Warn("fire 2xx but body unparseable",
|
||||
"cron", cronExpr,
|
||||
"status", resp.StatusCode,
|
||||
"body", string(respBody))
|
||||
return nil
|
||||
}
|
||||
c.Log.Info("fire ok",
|
||||
"cron", cronExpr,
|
||||
"session_url", fr.ClaudeCodeSessionURL,
|
||||
"session_id", fr.ClaudeCodeSessionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *FireClient) now() time.Time {
|
||||
if c.NowFunc != nil {
|
||||
return c.NowFunc()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (c *FireClient) renderText(now time.Time, cronExpr string) (string, error) {
|
||||
data := map[string]any{
|
||||
"Now": now,
|
||||
"LocalTime": now.In(c.TZ).Format("2006-01-02 15:04 MST"),
|
||||
"Cron": cronExpr,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := c.Template.Execute(&buf, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
Reference in new issue
Block a user