feat: add inbox

This commit is contained in:
2026-05-26 17:49:31 +08:00
parent f7fcd0e966
commit 68b5c8bc81
25 changed files with 2108 additions and 1 deletions
+1
View File
@@ -53,5 +53,6 @@ func RunClaude(r io.Reader, cfg config.Config, event string, w io.Writer) error
return err
}
logx.Append("hook claude event=%s terminalSequence OK title=%q", event, title)
recordInbox(cfg, "Claude", event, meta.CWD, title, body)
return nil
}
+1
View File
@@ -64,6 +64,7 @@ func RunCursor(r io.Reader, cfg config.Config, event string, _ io.Writer) error
return err
}
logx.Append("hook cursor event=%s send OK via %s title=%q", event, result.Method, title)
recordInbox(cfg, "Cursor", event, meta.CWD, title, body)
return nil
}
+104
View File
@@ -3,10 +3,12 @@ package hook
import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
"github.com/longbin/agent-notify/internal/config"
"github.com/longbin/agent-notify/internal/inbox"
)
func TestCursorStopHookDisabled(t *testing.T) {
@@ -46,3 +48,105 @@ func TestClaudeStopHookActiveSkips(t *testing.T) {
t.Fatalf("expected {}, got %q", out.String())
}
}
func TestCursorHookUploadsInboxRecord(t *testing.T) {
stubCursorSend(t)
var uploaded inbox.Record
stubInbox(t,
func(rec inbox.Record, cfg config.Config) error {
uploaded = rec
return nil
},
func(rec inbox.Record) error {
t.Fatalf("unexpected fallback append: %+v", rec)
return nil
},
)
cfg := config.Default()
err := RunCursor(bytes.NewReader([]byte(`{"workspace_roots":["/tmp/proj"]}`)), cfg, "stop", &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
if uploaded.Agent != "Cursor" || uploaded.Event != "stop" || uploaded.CWD != "/tmp/proj" {
t.Fatalf("unexpected upload record: %+v", uploaded)
}
}
func TestCursorHookFallbacksWhenInboxUploadFails(t *testing.T) {
stubCursorSend(t)
var fallback inbox.Record
stubInbox(t,
func(rec inbox.Record, cfg config.Config) error {
return errors.New("offline")
},
func(rec inbox.Record) error {
fallback = rec
return nil
},
)
cfg := config.Default()
err := RunCursor(bytes.NewReader([]byte(`{"workspace_roots":["/tmp/proj"]}`)), cfg, "stop", &bytes.Buffer{})
if err != nil {
t.Fatal(err)
}
if fallback.Source != inbox.SourceFallback || fallback.Title == "" {
t.Fatalf("unexpected fallback record: %+v", fallback)
}
}
func TestDisabledHookDoesNotRecordInbox(t *testing.T) {
stubCursorSend(t)
stubInbox(t,
func(rec inbox.Record, cfg config.Config) error {
t.Fatalf("unexpected upload: %+v", rec)
return nil
},
func(rec inbox.Record) error {
t.Fatalf("unexpected fallback: %+v", rec)
return nil
},
)
cfg := config.Default()
cfg.Events.Stop = false
if err := RunCursor(bytes.NewReader([]byte(`{}`)), cfg, "stop", &bytes.Buffer{}); err != nil {
t.Fatal(err)
}
}
func TestClaudeHookUploadsInboxRecord(t *testing.T) {
var uploaded inbox.Record
stubInbox(t,
func(rec inbox.Record, cfg config.Config) error {
uploaded = rec
return nil
},
func(rec inbox.Record) error {
t.Fatalf("unexpected fallback append: %+v", rec)
return nil
},
)
cfg := config.Default()
var out bytes.Buffer
if err := RunClaude(strings.NewReader(`{"stop_hook_active":false}`), cfg, "stop", &out); err != nil {
t.Fatal(err)
}
if uploaded.Agent != "Claude" || uploaded.Event != "stop" || uploaded.Title == "" {
t.Fatalf("unexpected upload record: %+v", uploaded)
}
}
func stubInbox(t *testing.T, upload func(inbox.Record, config.Config) error, appendLocal func(inbox.Record) error) {
t.Helper()
prevUpload := uploadInboxRecord
prevAppend := appendInboxRecord
uploadInboxRecord = upload
appendInboxRecord = appendLocal
t.Cleanup(func() {
uploadInboxRecord = prevUpload
appendInboxRecord = prevAppend
})
}
+53
View File
@@ -0,0 +1,53 @@
package hook
import (
"context"
"time"
"github.com/longbin/agent-notify/internal/config"
"github.com/longbin/agent-notify/internal/inbox"
"github.com/longbin/agent-notify/internal/logx"
)
var (
uploadInboxRecord = defaultUploadInboxRecord
appendInboxRecord = func(rec inbox.Record) error {
return inbox.NewStore("").Append(rec)
}
)
func recordInbox(cfg config.Config, agent, event, cwd, title, body string) {
if !cfg.Inbox.Enabled {
return
}
rec := inbox.BuildRecord(inbox.BuildInput{
Agent: agent,
Event: event,
CWD: cwd,
Title: title,
Body: body,
Source: inbox.SourceRemote,
})
if err := uploadInboxRecord(rec, cfg); err != nil {
logx.Append("inbox upload failed: %v", err)
if !cfg.Inbox.FallbackLocal {
return
}
rec.Source = inbox.SourceFallback
if err := appendInboxRecord(rec); err != nil {
logx.Append("inbox fallback append failed: %v", err)
}
}
}
func defaultUploadInboxRecord(rec inbox.Record, cfg config.Config) error {
timeout := time.Duration(cfg.Inbox.TimeoutMS) * time.Millisecond
client := inbox.NewClient(inbox.ClientConfig{
Socket: cfg.Inbox.RemoteSocket,
Addr: cfg.Inbox.Addr,
Timeout: timeout,
})
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return client.Upload(ctx, rec)
}