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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- Nanobot integration: `mnemon setup --target nanobot` deploys a skill file to
`.nanobot/skills/mnemon/SKILL.md` (local) or `~/.nanobot/workspace/skills/mnemon/SKILL.md`
(global, recommended). `mnemon setup --eject` removes it. Detection is automatic
when the `nanobot` binary or `~/.nanobot/workspace/` directory is present.

## [0.1.5] - 2026-05-17

### Fixed
Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ NanoClaw runs agents inside Linux containers. Use the `/add-mnemon` skill to int

The skill is available at `.claude/skills/add-mnemon/` in the NanoClaw repo.

### [Nanobot](https://github.com/HKUDS/nanobot)

```bash
mnemon setup --target nanobot --global --yes
```

One command writes a skill file to `~/.nanobot/workspace/skills/mnemon/SKILL.md`. Memory is shared across all Nanobot sessions and projects. Use `--global` (recommended) because Nanobot discovers skills from the global workspace directory.

### Uninstall

```bash
Expand Down Expand Up @@ -156,6 +164,7 @@ memory is useful.

- **Zero user-side operation** — install once; supported runtimes can use hooks, minimal runtimes can use persistent rules
- **LLM-supervised** — the host LLM decides what to remember, update, and forget; no embedded LLM, no API keys
- **Multi-framework support** — Claude Code (hooks), OpenClaw (plugins), Nanobot (skills), and more
- **Markdown-installable harness** — `SKILL.md`, `INSTALL.md`, `GUIDELINE.md`, and four lifecycle reminders
- **Four-graph architecture** — temporal, entity, causal, and semantic edges, not just vector similarity
- **Intent-native protocol** — three primitives (`remember`, `link`, `recall`) map to the LLM's cognitive vocabulary, not database syntax; structured JSON output with signal transparency
Expand All @@ -173,6 +182,8 @@ All your local agentic AIs — across sessions and frameworks — sharing one po
OpenClaw ─────┤
Nanobot ──────┤
NanoClaw ─────┤
├──▶ ~/.mnemon ◀── shared memory
OpenCode ─────┤
Expand All @@ -182,9 +193,9 @@ All your local agentic AIs — across sessions and frameworks — sharing one po

The foundation is in place: a single `~/.mnemon` database that any agent can
read and write. Claude Code setup automates hook installation; OpenClaw can use
plugin hooks; NanoClaw integrates via container skills and volume mounts. The
same harness can be installed in any LLM CLI that supports skills, rules,
system prompts, or event hooks.
plugin hooks; Nanobot integrates via skill files; NanoClaw integrates via
container skills and volume mounts. The same harness can be installed in any
LLM CLI that supports skills, rules, system prompts, or event hooks.

The longer-term direction is a **memory gateway**: protocol decoupled from storage engine. The current SQLite backend is the first adapter; the protocol surface (`remember / link / recall`) can sit on top of PostgreSQL, Neo4j, or any graph database. Agent-side optimization (when to recall, what to remember) and storage-side optimization (indexing, graph algorithms) evolve independently. See [Future Direction](docs/design/08-decisions.md#82-future-direction) for details.

Expand Down
78 changes: 71 additions & 7 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ var setupCmd = &cobra.Command{
Short: "Deploy mnemon into LLM CLI environments",
Long: `Detect installed LLM CLIs and deploy mnemon integration.

By default, installs to project-local config (.claude/, .openclaw/).
Use --global to install to user-wide config (~/.claude/, ~/.openclaw/).
By default, installs to project-local config (.claude/, .openclaw/, .nanobot/).
Use --global to install to user-wide config (~/.claude/, ~/.openclaw/, ~/.nanobot/workspace/).

Supported environments: Claude Code, OpenClaw.
Supported environments: Claude Code, OpenClaw, Nanobot.

Examples:
mnemon setup # Interactive: project-local install
Expand All @@ -38,16 +38,16 @@ Examples:
}

func init() {
setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, openclaw)")
setupCmd.Flags().StringVar(&setupTarget, "target", "", "target environment (claude-code, openclaw, nanobot)")
setupCmd.Flags().BoolVar(&setupEject, "eject", false, "remove mnemon integrations")
setupCmd.Flags().BoolVar(&setupYes, "yes", false, "auto-confirm all prompts (CI-friendly)")
setupCmd.Flags().BoolVar(&setupGlobal, "global", false, "install to user-wide config (~/.claude/) instead of project-local (.claude/)")
rootCmd.AddCommand(setupCmd)
}

func runSetup(cmd *cobra.Command, args []string) error {
if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "openclaw" {
return fmt.Errorf("invalid target %q (must be claude-code or openclaw)", setupTarget)
if setupTarget != "" && setupTarget != "claude-code" && setupTarget != "openclaw" && setupTarget != "nanobot" {
return fmt.Errorf("invalid target %q (must be claude-code, openclaw, or nanobot)", setupTarget)
}

envs := setup.DetectEnvironments(setupGlobal)
Expand Down Expand Up @@ -83,7 +83,7 @@ func runInstallFlow(envs []setup.Environment) error {

if len(detected) == 0 {
fmt.Println("\nNo supported LLM CLI environments detected.")
fmt.Println("Install Claude Code or OpenClaw, then run 'mnemon setup' again.")
fmt.Println("Install Claude Code, OpenClaw, or Nanobot, then run 'mnemon setup' again.")
return nil
}

Expand Down Expand Up @@ -127,6 +127,8 @@ func installEnv(env *setup.Environment) error {
err = installClaudeCode(env)
case "openclaw":
err = installOpenClaw(env)
case "nanobot":
err = installNanobot(env)
}
if err != nil {
return err
Expand Down Expand Up @@ -397,6 +399,61 @@ func selectOpenClawOptionalHooks() setup.HookSelection {
return sel
}

// ─── Nanobot ────────────────────────────────────────────────────────

func installNanobot(env *setup.Environment) error {
configDir := env.ConfigDir

// Scope selection
if !setupGlobal && !setupYes && setup.IsInteractive() {
home := setup.HomeDir()
localDir := ".nanobot"
globalDir := home + "/.nanobot/workspace"
idx := setup.SelectOne("Install scope",
[]string{
fmt.Sprintf("Global — all projects (%s/)", globalDir),
fmt.Sprintf("Local — this project only (%s/)", localDir),
}, 0) // default: Global
if idx == 1 {
configDir = localDir
} else {
configDir = globalDir
}
}

fmt.Printf("\nSetting up Nanobot (%s)...\n", configDir)

// Phase 1: Skill
fmt.Println("\n[1/2] Skill")
if path, err := setup.NanobotWriteSkill(configDir); err != nil {
setup.StatusError(0, 0, "Skill", err)
return err
} else {
setup.StatusOK(0, 0, "Skill", path)
}

// Phase 2: Prompt files (guide.md + skill.md → ~/.mnemon/prompt/)
fmt.Println("\n[2/2] Prompts")
if path, err := setup.WritePromptFiles(); err != nil {
setup.StatusError(0, 0, "Prompts", err)
return err
} else {
setup.StatusOK(0, 0, "Prompts", path)
}

// Summary
fmt.Println()
fmt.Println("Setup complete!")
fmt.Printf(" Skill %s/skills/mnemon/SKILL.md\n", configDir)
fmt.Printf(" Prompts ~/.mnemon/prompt/ (guide.md, skill.md)\n")
fmt.Println()
fmt.Println("Restart Nanobot to activate the mnemon skill.")
fmt.Println("Edit ~/.mnemon/prompt/guide.md to customize behavior.")
fmt.Println("Run 'mnemon setup --eject' to remove.")

return nil
}

// ─── Eject ──────────────────────────────────────────────────────────

func runEjectFlow(envs []setup.Environment) error {
Expand Down Expand Up @@ -475,6 +532,13 @@ func ejectEnv(env *setup.Environment) error {
if len(errs) > 0 {
return errs[0]
}

case "nanobot":
errs := setup.NanobotEject(env.ConfigDir)
ejectMarkdown("AGENTS.md", "Remove memory guidance from ./AGENTS.md?")
if len(errs) > 0 {
return errs[0]
}
}
return nil
}
Expand Down
5 changes: 3 additions & 2 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ mnemon setup --global
# Non-interactive: specific target only
mnemon setup --target claude-code
mnemon setup --target openclaw
mnemon setup --target nanobot --global

# Auto-confirm all prompts (CI-friendly)
mnemon setup --yes
Expand All @@ -41,8 +42,8 @@ mnemon setup --eject --target claude-code

| Flag | Default | Description |
|---|---|---|
| `--global` | `false` | Install to user-wide config (`~/.claude/`) instead of project-local (`.claude/`) |
| `--target <name>` | (auto-detect) | Target environment: `claude-code` or `openclaw` |
| `--global` | `false` | Install to user-wide config instead of project-local (required for Nanobot: installs to `~/.nanobot/workspace/`) |
| `--target <name>` | (auto-detect) | Target environment: `claude-code`, `openclaw`, or `nanobot` |
| `--eject` | `false` | Remove mnemon integrations |
| `--yes` | `false` | Auto-confirm all prompts |

Expand Down
5 changes: 4 additions & 1 deletion internal/setup/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ var NanoClawSkill []byte
//go:embed nanoclaw/container-skill.md
var NanoClawContainerSkill []byte

//go:embed nanobot/SKILL.md
var NanobotSkill []byte

// All returns the embedded filesystem for inspection.
//
//go:embed claude openclaw nanoclaw
//go:embed claude openclaw nanoclaw nanobot
var All embed.FS
53 changes: 53 additions & 0 deletions internal/setup/assets/nanobot/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: mnemon
description: Persistent memory CLI for LLM agents. Store facts, recall past knowledge, link related memories, manage lifecycle.
---

# mnemon

## Workflow

1. **Remember**: `mnemon remember "<fact>" --cat <cat> --imp <1-5> --entities "e1,e2" --source agent`
- Diff is built-in: duplicates skipped, conflicts auto-replaced.
- Output includes `action` (added/updated/skipped), `semantic_candidates`, `causal_candidates`.
2. **Link** (evaluate candidates from step 1 — use judgment, not mechanical rules):
- Review `causal_candidates`: does a genuine cause-effect relationship exist? `causal_signal` is regex-based and prone to false positives — only link if the memories are truly causally related.
- Review `semantic_candidates`: are these memories meaningfully related? High `similarity` alone is not sufficient — skip candidates that share keywords but discuss unrelated topics.
- Syntax: `mnemon link <id> <candidate> --type <causal|semantic> --weight <0-1> [--meta '<json>']`
3. **Recall**: `mnemon recall "<query>" --limit 10`

## Commands

```bash
mnemon remember "<fact>" --cat <cat> --imp <1-5> --entities "e1,e2" --source agent
mnemon link <id1> <id2> --type <type> --weight <0-1> [--meta '<json>']
mnemon recall "<query>" --limit 10
mnemon search "<query>" --limit 10
mnemon forget <id>
mnemon related <id> --edge causal
mnemon gc --threshold 0.4
mnemon gc --keep <id>
mnemon status
mnemon log
mnemon store list
mnemon store create <name>
mnemon store set <name>
mnemon store remove <name>
```

## Usage with nanobot

Use the `exec` tool to run mnemon commands. Recall can run in the main conversation; delegate `remember` and `link` to a sub-agent via `spawn` to keep the main conversation clean.

```
exec(command="mnemon recall 'user preferences'")
exec(command="mnemon recall 'past decisions about auth'")
```

## Guardrails

- Prefer delegating `remember` and `link` to a sub-agent via `spawn` rather than running them in the main conversation.
- Do not store secrets, passwords, or tokens.
- Categories: `preference` · `decision` · `insight` · `fact` · `context`
- Edge types: `temporal` · `semantic` · `causal` · `entity`
- Max 8,000 chars per insight.
42 changes: 42 additions & 0 deletions internal/setup/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func DetectEnvironments(global bool) []Environment {
return []Environment{
detectClaude(global),
detectOpenClaw(global),
detectNanobot(global),
}
}

Expand Down Expand Up @@ -123,3 +124,44 @@ func cleanVersion(v string) string {
}
return v
}

func detectNanobot(global bool) Environment {
home := HomeDir()
globalDir := filepath.Join(home, ".nanobot", "workspace")
localDir := ".nanobot"

configDir := localDir
if global {
configDir = globalDir
}

env := Environment{
Name: "nanobot",
Display: "Nanobot",
ConfigDir: configDir,
}

// CLI detection is always global
if binPath, err := exec.LookPath("nanobot"); err == nil {
env.Detected = true
env.BinPath = binPath
}
if _, err := os.Stat(globalDir); err == nil {
env.Detected = true
}

// Check if mnemon integration is already installed at the target location
skillPath := filepath.Join(configDir, "skills", "mnemon", "SKILL.md")
if _, err := os.Stat(skillPath); err == nil {
env.Installed = true
}

// Get version
if env.BinPath != "" {
if out, err := exec.Command(env.BinPath, "--version").Output(); err == nil {
env.Version = cleanVersion(strings.TrimSpace(string(out)))
}
}

return env
}
44 changes: 44 additions & 0 deletions internal/setup/nanobot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package setup

import (
"fmt"
"os"
"path/filepath"

"github.com/mnemon-dev/mnemon/internal/setup/assets"
)

// NanobotWriteSkill writes the SKILL.md to the nanobot skills directory.
func NanobotWriteSkill(configDir string) (string, error) {
skillDir := filepath.Join(configDir, "skills", "mnemon")
if err := os.MkdirAll(skillDir, 0755); err != nil {
return "", err
}
skillPath := filepath.Join(skillDir, "SKILL.md")
if err := os.WriteFile(skillPath, assets.NanobotSkill, 0644); err != nil {
return "", err
}
return skillPath, nil
}

// NanobotEject removes mnemon skill from the given nanobot config dir.
func NanobotEject(configDir string) []error {
var errs []error

fmt.Printf("\nRemoving nanobot integration (%s)...\n", configDir)

// Remove skill directory
skillDir := filepath.Join(configDir, "skills", "mnemon")
if err := os.RemoveAll(skillDir); err != nil {
StatusError(1, 1, "Skill", err)
errs = append(errs, err)
} else {
StatusOK(1, 1, "Skill", skillDir+" removed")
}
removeIfEmpty(filepath.Join(configDir, "skills"))

// Clean up configDir itself if empty
removeIfEmpty(configDir)

return errs
}
Loading