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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,17 @@ jobs:
go-version-file: go.mod
cache-dependency-path: go.sum

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22.19.0

- name: Run deterministic test suite
run: make test

- name: Test npm distribution tooling
run: npm test --prefix npm/cli

windows-memory:
runs-on: windows-latest
timeout-minutes: 15
Expand All @@ -49,8 +57,16 @@ jobs:
go-version-file: go.mod
cache-dependency-path: go.sum

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22.19.0

- name: Build Windows Memory product
run: go build -o mnemon.exe .

- name: Test Windows command and Memory storage boundaries
run: go test ./cmd ./cmd/agency ./cmd/memory ./internal/memory/store -count=1

- name: Test Windows npm tooling
run: npm test --prefix npm/cli
59 changes: 58 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- "v*"

permissions:
contents: write
contents: read

concurrency:
group: release-${{ github.ref }}
Expand All @@ -23,12 +23,22 @@ jobs:
with:
go-version-file: go.mod

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22.19.0

- name: Run deterministic test suite
run: make test

- name: Test npm distribution tooling
run: npm test --prefix npm/cli

release:
needs: test
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -48,3 +58,50 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}

- name: Preserve native binaries for npm packaging
uses: actions/upload-artifact@v4
with:
name: npm-release-binaries
path: |
dist/artifacts.json
dist/mnemon_*/*
if-no-files-found: error
retention-days: 1

npm-release:
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4

- name: Set up Node and npm registry authentication
uses: actions/setup-node@v6
with:
node-version: 24.20.0
registry-url: https://registry.npmjs.org

- name: Download native release binaries
uses: actions/download-artifact@v4
with:
name: npm-release-binaries
path: dist

- name: Stage npm packages
run: >-
node scripts/build-npm-packages.mjs
--version "${{ github.ref_name }}"
--dist dist
--output dist/npm

- name: Verify npm packages and launcher
run: node scripts/verify-npm-packages.mjs dist/npm/packages.json

- name: Publish platform artifacts, then the CLI package
run: node scripts/publish-npm-packages.mjs dist/npm/packages.json
env:
# Used only to bootstrap the package; trusted publishing uses OIDC.
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

### Added

- `npm install --global @mnemon-dev/mnemon` is now the canonical CLI install
path. Tagged releases publish pinned native artifacts for macOS, Linux, and
Windows before advancing the npm `latest` or `next` channel.
- `mnemon update` upgrades an npm-managed installation through its owning npm
prefix. The npm launcher performs replacement without keeping the native
process running, including on Windows. Installations from another source fail
closed with a one-time npm migration command instead of silently creating a
shadowed executable.
- `mnemon recall --brief` and `mnemon search --brief` now provide a bounded,
unindented JSON discovery projection. `--excerpt-chars` controls the per-item
excerpt limit, and `mnemon show <id>` retrieves one selected insight in full.
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ git push origin v0.2.0

This triggers GitHub Actions → runs tests → builds platform artifacts for the
single `mnemon` executable via GoReleaser → publishes a GitHub Release →
updates the Homebrew tap.
updates the Homebrew tap → verifies and publishes the npm platform artifacts →
publishes `@mnemon-dev/mnemon` last.

## License

Expand Down
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,35 @@ See [Design & Architecture](docs/DESIGN.md) for details.

### Install

**Homebrew Cask** (macOS):
**npm** (recommended; macOS / Linux / Windows, Node.js 22+):

```bash
brew install --cask mnemon-dev/tap/mnemon
npm install --global @mnemon-dev/mnemon
```

Upgrade the npm-managed CLI at any time:

```bash
mnemon update
```

**Go install** (macOS / Linux / Windows):
The npm package installs the matching native Go executable for the host OS and
CPU. Mnemon's engine remains a single native binary; Node.js is used only by
the npm launcher and package manager.

**Alternative installers**:

```bash
brew install --cask mnemon-dev/tap/mnemon
go install github.com/mnemon-dev/mnemon@latest
```

Homebrew, `go install`, source builds, and other Node package managers must
continue to use their original installation method. To migrate one of these
installations, run the npm install command once and ensure the npm global bin
directory precedes the old executable on `PATH`; subsequent `mnemon update`
calls are npm-managed.

Windows supports the core Memory commands. Agency remains unavailable on
Windows until its local authority boundary has native Windows security.

Expand Down
21 changes: 14 additions & 7 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ func Execute(ctx context.Context, args []string, stdin io.Reader, stdout, stderr
}
root := productRoot()
agencyRequest := false
quietProductRequest := false
command, _, findErr := root.Find(args)
if findErr == nil {
agencyRequest = belongsToAgency(command)
if agencyRequest {
quietProductRequest = belongsToCommand(command, "update")
if agencyRequest || quietProductRequest {
root.SilenceErrors = true
}
}
Expand All @@ -41,7 +43,8 @@ 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 {
if findErr == nil && !agencyRequest && !quietProductRequest &&
!belongsToAgency(executed) && executed != nil {
_, _ = fmt.Fprintln(stderr, executed.UsageString())
}
if err.Error() != "" {
Expand All @@ -57,8 +60,12 @@ func Execute(ctx context.Context, args []string, stdin io.Reader, stdout, stderr
}

func belongsToAgency(command *cobra.Command) bool {
return belongsToCommand(command, "agency")
}

func belongsToCommand(command *cobra.Command, name string) bool {
for current := command; current != nil; current = current.Parent() {
if current.Name() == "agency" {
if current.Name() == name {
return true
}
}
Expand All @@ -71,14 +78,14 @@ func productRoot() *cobra.Command {
root.Long = "Mnemon gives LLM agents persistent memory and a local authority for durable, peer-to-peer work."
root.SilenceErrors = false
root.SilenceUsage = false
// Memory's current command tree is process-global. Remove a prior command
// so focused tests can construct the product root more than once without
// Memory's current command tree is process-global. Remove prior product
// commands so focused tests can construct the root more than once without
// changing the production command set.
for _, child := range root.Commands() {
if child.Name() == "agency" {
if child.Name() == "agency" || child.Name() == "update" {
root.RemoveCommand(child)
}
}
root.AddCommand(agency.New(version))
root.AddCommand(agency.New(version), updateCommand())
return root
}
14 changes: 13 additions & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

func TestRootComposesMemoryAndAgency(t *testing.T) {
root := productRoot()
for _, name := range []string{"remember", "recall", "setup", "agency"} {
for _, name := range []string{"remember", "recall", "setup", "agency", "update"} {
child, _, err := root.Find([]string{name})
if err != nil || child == root {
t.Fatalf("root command %q is not registered", name)
Expand All @@ -21,6 +21,18 @@ func TestRootComposesMemoryAndAgency(t *testing.T) {
}
}

func TestUnmanagedUpdateExplainsTheOneTimeNPMMigration(t *testing.T) {
var stdout, stderr bytes.Buffer
exitCode := Execute(context.Background(), []string{"update"}, strings.NewReader(""),
&stdout, &stderr)
if exitCode != 1 || stdout.Len() != 0 ||
!strings.Contains(stderr.String(), "npm install --global @mnemon-dev/mnemon@latest") ||
strings.Contains(stderr.String(), "Usage:") {
t.Fatalf("unmanaged update: exit=%d stdout=%q stderr=%q",
exitCode, stdout.String(), stderr.String())
}
}

func TestExecuteRoutesAgencyWithoutChangingItsExitCode(t *testing.T) {
var stdout, stderr bytes.Buffer
exitCode := Execute(context.Background(), []string{"agency", "--version"},
Expand Down
19 changes: 19 additions & 0 deletions cmd/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cmd

import (
"errors"

"github.com/spf13/cobra"
)

func updateCommand() *cobra.Command {
return &cobra.Command{
Use: "update",
Short: "Update the npm-managed Mnemon CLI",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
return errors.New("this Mnemon installation is not managed by npm; " +
"migrate once with: npm install --global @mnemon-dev/mnemon@latest")
},
}
}
21 changes: 21 additions & 0 deletions cmd/update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package cmd

import (
"context"
"io"
"strings"
"testing"
)

func TestNativeUpdateCommandRequiresNPMLauncher(t *testing.T) {
t.Parallel()
command := updateCommand()
command.SetOut(io.Discard)
command.SetErr(io.Discard)
command.SetArgs(nil)
err := command.ExecuteContext(context.Background())
if err == nil ||
!strings.Contains(err.Error(), "npm install --global @mnemon-dev/mnemon@latest") {
t.Fatalf("error = %v", err)
}
}
41 changes: 39 additions & 2 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Prerequisites:

- Go 1.24.6 or newer in the 1.24 series
- Node.js 22 or newer for npm package tests
- `make`
- `jq` only when running the opt-in CLI E2E/integration suite

Expand Down Expand Up @@ -102,14 +103,50 @@ server is not on a trusted local network.

## Release Deployment

Tagged releases are handled by GoReleaser through `.github/workflows/release.yml`.
Tagged releases are handled through `.github/workflows/release.yml`. GoReleaser
publishes the native GitHub artifacts and Homebrew cask first. A dependent job
then stages those exact binaries as npm artifacts, verifies the host launcher,
and publishes the canonical `@mnemon-dev/mnemon` package.

Required repository secret:
Long-lived repository secret:

- `HOMEBREW_TAP_TOKEN`, only needed for publishing the Homebrew tap

Before the first npm release, reserve the `@mnemon-dev` npm scope and add a
granular `NPM_TOKEN` repository secret that can bootstrap the public
`@mnemon-dev/mnemon` package. After that first tagged release:

1. Configure `mnemon-dev/mnemon` and `release.yml` as the package's GitHub
Actions [trusted publisher](https://docs.npmjs.com/trusted-publishers/) on
npm, allowing direct `npm publish`.
2. Run the next tagged release and confirm that npm records GitHub Actions as
its trusted publisher.
3. Delete the `NPM_TOKEN` repository secret and revoke the bootstrap token.

The npm release job uses Node.js 24 and requests only the OIDC permission needed
for token-free trusted publishing. npm automatically binds each publish to the
workflow and records provenance; `--provenance` also covers the one-time token
bootstrap release.

One tag is the only version source. For `v0.3.0`, the workflow publishes six
platform versions such as `0.3.0-darwin-arm64`, then publishes the `0.3.0` CLI
meta-package last. The meta-package pins each platform version through npm
aliases. Stable tags advance `latest`; prerelease tags advance `next`. Publishing
the meta-package last prevents npm users from observing an incomplete release.

Publishing is retry-safe: already published immutable versions are skipped, so
the failed `npm-release` job can be rerun without rebuilding or republishing the
GitHub release.

Create a local snapshot build without publishing:

```bash
make release-snapshot
```

The npm staging tools consume GoReleaser's `dist/artifacts.json`; they do not
maintain a second build matrix. They are exercised independently with:

```bash
npm test --prefix npm/cli
```
22 changes: 22 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ the read-only SQLite file URI internally; do not prepend `file:` yourself.

---

## CLI Updates

The canonical npm installation can update itself to the package tagged
`latest`:

```bash
mnemon update
```

The npm launcher proves that the active package belongs to the same global npm
prefix before invoking npm. It fails closed when `mnemon` came from Homebrew,
`go install`, a source build, another Node package manager, or a different npm
prefix, preventing a second installation from being created silently. Migrate
once with `npm install --global @mnemon-dev/mnemon@latest`, then make sure that
npm's global bin directory is the first `mnemon` on `PATH`.

Updating replaces only the CLI package. It does not modify Memory data or
silently rewrite installed host integrations. Review release notes and rerun
`mnemon setup` when an integration release explicitly requires a refresh.

---

## Memory Setup

Deploy mnemon into LLM CLI environments. This is the first command to run after installation.
Expand Down
Loading
Loading