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
6 changes: 6 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ homebrew_casks:
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
homepage: "https://github.com/mnemon-dev/mnemon"
description: "Persistent memory and durable agency for LLM agents"
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/mnemon"]
end

release:
github:
Expand Down
6 changes: 5 additions & 1 deletion cmd/agency/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"errors"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/mnemon-dev/mnemon/internal/daemon"
Expand Down Expand Up @@ -37,7 +39,9 @@ func runServe(command *cobra.Command, _ []string) error {
}
resolved, err := resolveStateDirectory(stateDirectory)
if err == nil {
err = serveDaemon(command.Context(), resolved)
lifetime, stop := signal.NotifyContext(command.Context(), os.Interrupt, syscall.SIGTERM)
defer stop()
err = serveDaemon(lifetime, resolved)
}
if err != nil {
return commandFailure{code: 1, err: fmt.Errorf("mnemon agency serve: %w", err)}
Expand Down
40 changes: 27 additions & 13 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,26 @@ import (

var version = "dev"

// Execute runs one Mnemon command. Process signal handling and exit remain the
// root main package's responsibility.
// Execute runs one Mnemon command. Process exit remains the root main package's
// responsibility; a long-running command owns any graceful signal handling it
// requires so ordinary Memory commands retain the operating system defaults.
func Execute(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
if ctx == nil || stdin == nil || stdout == nil || stderr == nil {
return 1
}
root := productRoot()
if command, _, findErr := root.Find(args); findErr == nil {
for current := command; current != nil; current = current.Parent() {
if current.Name() == "agency" {
root.SilenceErrors = true
root.SilenceUsage = true
break
}
agencyRequest := false
command, _, findErr := root.Find(args)
if findErr == nil {
agencyRequest = belongsToAgency(command)
if agencyRequest {
root.SilenceErrors = true
}
}
// Cobra renders automatic usage through the command output writer. Keep
// successful help and version output on stdout, but render Memory's usage
// explicitly to stderr after an execution error, as the established CLI did.
root.SilenceUsage = true
root.SetArgs(args)
root.SetIn(stdin)
root.SetOut(stdout)
Expand All @@ -37,20 +41,30 @@ func Execute(ctx context.Context, args []string, stdin io.Reader, stdout, stderr
if err == nil {
return 0
}
if findErr == nil && !agencyRequest && !belongsToAgency(executed) && executed != nil {
_, _ = fmt.Fprintln(stderr, executed.UsageString())
}
if err.Error() != "" {
_, _ = fmt.Fprintln(stderr, err)
}
if code, ok := agency.ExitCode(err); ok {
return code
}
for command := executed; command != nil; command = command.Parent() {
if command.Name() == "agency" {
return 2
}
if agencyRequest || belongsToAgency(executed) {
return 2
}
return 1
}

func belongsToAgency(command *cobra.Command) bool {
for current := command; current != nil; current = current.Parent() {
if current.Name() == "agency" {
return true
}
}
return false
}

func productRoot() *cobra.Command {
root := memory.New(version)
root.Short = "Memory and durable agency for LLM agents"
Expand Down
29 changes: 27 additions & 2 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,34 @@ func TestMemoryKeepsItsExistingCobraErrorOutput(t *testing.T) {
var stdout, stderr bytes.Buffer
exitCode := Execute(context.Background(), []string{"forget"},
strings.NewReader(""), &stdout, &stderr)
if exitCode != 1 || !strings.Contains(stderr.String(), "Error:") ||
!strings.Contains(stdout.String(), "Usage:") {
if exitCode != 1 || stdout.Len() != 0 ||
!strings.Contains(stderr.String(), "Error:") ||
!strings.Contains(stderr.String(), "Usage:") ||
!strings.Contains(stderr.String(), "\n\naccepts 1 arg(s), received 0\n") {
t.Fatalf("memory usage error: exit=%d stdout=%q stderr=%q",
exitCode, stdout.String(), stderr.String())
}
}

func TestMemoryHelpRemainsSuccessfulStdout(t *testing.T) {
var stdout, stderr bytes.Buffer
exitCode := Execute(context.Background(), []string{"forget", "--help"},
strings.NewReader(""), &stdout, &stderr)
if exitCode != 0 || !strings.Contains(stdout.String(), "mnemon forget [id]") ||
stderr.Len() != 0 {
t.Fatalf("memory help: exit=%d stdout=%q stderr=%q",
exitCode, stdout.String(), stderr.String())
}
}

func TestUnknownRootCommandKeepsTheShortCobraDiagnostic(t *testing.T) {
var stdout, stderr bytes.Buffer
exitCode := Execute(context.Background(), []string{"unknown"},
strings.NewReader(""), &stdout, &stderr)
if exitCode != 1 || stdout.Len() != 0 ||
!strings.Contains(stderr.String(), "Run 'mnemon --help' for usage.") ||
strings.Contains(stderr.String(), "Usage:\n") {
t.Fatalf("unknown command: exit=%d stdout=%q stderr=%q",
exitCode, stdout.String(), stderr.String())
}
}
6 changes: 1 addition & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@ package main
import (
"context"
"os"
"os/signal"
"syscall"

"github.com/mnemon-dev/mnemon/cmd"
)

func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
exitCode := cmd.Execute(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr)
stop()
exitCode := cmd.Execute(context.Background(), os.Args[1:], os.Stdin, os.Stdout, os.Stderr)
if exitCode != 0 {
os.Exit(exitCode)
}
Expand Down
Loading