@xultrax-web/agent-memory-mcp 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -272,6 +272,7 @@ agent-memory sync init git@github.com:you/agent-memory.git # multi-machine se
272
272
  agent-memory sync push # commit + push local changes
273
273
  agent-memory sync pull # fast-forward from remote
274
274
  agent-memory sync status # local + ahead/behind state
275
+ agent-memory ui # launch the TUI (browse + edit interactively)
275
276
  ```
276
277
 
277
278
  ### Multi-machine memory (git sync)
@@ -418,6 +419,16 @@ This server is built to be used daily, not to demo well once.
418
419
  - **`find_backlinks`** tool + `agent-memory backlinks <name>` CLI — "what links to this".
419
420
  - **`find_related`** tool + `agent-memory related <name>` CLI — combines outbound + inbound links, shared tags, type match, and content similarity into a ranked discovery view.
420
421
 
422
+ **Shipped in v0.10 · the visual identity (TUI):**
423
+
424
+ - **`agent-memory ui`** — Ink-based terminal UI for browsing, filtering, searching, and editing memories without leaving the terminal.
425
+ - Type-filter quick-keys (0-4 cycle through all/user/feedback/project/reference)
426
+ - Fuzzy live search with `/`
427
+ - `e` opens the highlighted memory in `$EDITOR` (vim/notepad/nano/whatever) — saves back to disk
428
+ - `d` soft-deletes with `y/n` confirmation
429
+ - Detail pane previews the body of the selected memory
430
+ - Color-coded by type, tag chips inline
431
+
421
432
  **Shipped in v0.9 · the moat — multi-machine memory via git:**
422
433
 
423
434
  - **`agent-memory sync init <remote-url>`** — convert `.agent-memory/` into a git repo, push to remote.
@@ -429,11 +440,12 @@ This server is built to be used daily, not to demo well once.
429
440
  - Per-machine state (`.lock`, `.events.jsonl`, `.trash/`) auto-excluded from sync.
430
441
  - Default commit identity injected (`agent-memory@local`) so machines without `git config --global user.email` work without setup.
431
442
 
432
- **Landing in v0.10+:**
443
+ **Landing in v0.11+:**
433
444
 
434
- - TUI / web UI for browsing + editing memories in a clean interface
435
445
  - Folder support (`.agent-memory/work/`, `.agent-memory/personal/`)
436
446
  - Memory packs for shareable curated bundles
447
+ - Web UI for browser-based memory browsing (companion to the TUI)
448
+ - Auto-context loading (LLM gets relevant memories transparently before each prompt)
437
449
 
438
450
  ---
439
451
 
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ import { spawnSync } from "node:child_process";
30
30
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from "node:fs";
31
31
  import { homedir } from "node:os";
32
32
  import { join, resolve } from "node:path";
33
+ import { fileURLToPath } from "node:url";
33
34
  import lockfile from "proper-lockfile";
34
35
  // -------------------------------------------------------------
35
36
  // Storage location resolution
@@ -43,7 +44,7 @@ function resolveStorageDir() {
43
44
  }
44
45
  return resolve(process.cwd(), ".agent-memory");
45
46
  }
46
- const MEMORY_DIR = resolveStorageDir();
47
+ export const MEMORY_DIR = resolveStorageDir();
47
48
  const INDEX_FILE = join(MEMORY_DIR, "MEMORY.md");
48
49
  const TRASH_DIR = join(MEMORY_DIR, ".trash");
49
50
  const LOCK_FILE = join(MEMORY_DIR, ".lock");
@@ -178,10 +179,10 @@ const SLUG_PATTERN = /^[a-z0-9][a-z0-9_-]{0,80}$/;
178
179
  const TAG_PATTERN = /^[a-z0-9][a-z0-9_-]{0,40}$/;
179
180
  // Wiki-links: [[memory-name]] · names follow SLUG_PATTERN rules
180
181
  const WIKI_LINK_PATTERN = /\[\[([a-z0-9][a-z0-9_-]{0,80})\]\]/g;
181
- function memoryFilePath(name) {
182
+ export function memoryFilePath(name) {
182
183
  return join(MEMORY_DIR, `${name}.md`);
183
184
  }
184
- function readMemory(name) {
185
+ export function readMemory(name) {
185
186
  const fp = memoryFilePath(name);
186
187
  if (!existsSync(fp))
187
188
  return null;
@@ -197,7 +198,7 @@ function readMemory(name) {
197
198
  filePath: fp,
198
199
  };
199
200
  }
200
- function listMemoryFiles() {
201
+ export function listMemoryFiles() {
201
202
  if (!existsSync(MEMORY_DIR))
202
203
  return [];
203
204
  return readdirSync(MEMORY_DIR)
@@ -576,7 +577,7 @@ function toolRelevantMemories(args) {
576
577
  }
577
578
  return sections.join("\n");
578
579
  }
579
- function toolDeleteMemory(args) {
580
+ export function toolDeleteMemory(args) {
580
581
  const name = String(args.name ?? "").trim();
581
582
  if (!SLUG_PATTERN.test(name))
582
583
  throw new Error(`Invalid name "${name}".`);
@@ -1235,7 +1236,7 @@ function actionColor(action) {
1235
1236
  // -------------------------------------------------------------
1236
1237
  // Server wiring
1237
1238
  // -------------------------------------------------------------
1238
- const server = new Server({ name: "agent-memory", version: "0.9.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
1239
+ const server = new Server({ name: "agent-memory", version: "0.10.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
1239
1240
  // -------------------------------------------------------------
1240
1241
  // Resource URI scheme
1241
1242
  // -------------------------------------------------------------
@@ -1761,6 +1762,7 @@ const CLI_COMMANDS = new Set([
1761
1762
  "backlinks",
1762
1763
  "related",
1763
1764
  "sync",
1765
+ "ui",
1764
1766
  "import-claude-code",
1765
1767
  "help",
1766
1768
  "--help",
@@ -1953,6 +1955,13 @@ async function cliMain(command, rest) {
1953
1955
  }) + "\n");
1954
1956
  return 0;
1955
1957
  }
1958
+ case "ui": {
1959
+ // Dynamic import so Ink + React only load when the TUI runs,
1960
+ // keeping cold-start fast for MCP server + every other CLI command.
1961
+ const { runTui } = await import("./tui.js");
1962
+ await runTui();
1963
+ return 0;
1964
+ }
1956
1965
  case "import-claude-code": {
1957
1966
  return importClaudeCode({
1958
1967
  source: flags.source ? String(flags.source) : undefined,
@@ -2010,6 +2019,8 @@ COMMANDS
2010
2019
  sync pull Fast-forward pull from remote.
2011
2020
  sync status Show local + ahead/behind state.
2012
2021
  sync log [--limit N] Recent sync commit history.
2022
+ ui Launch the TUI · browse, filter, search, edit memories
2023
+ in a clean terminal interface (Ink-based).
2013
2024
  import-claude-code [--source <path>] [--project <pat>] [--overwrite] [--dry-run]
2014
2025
  Walk ~/.claude/projects/*/memory/ and
2015
2026
  import each memory into the current store.
@@ -2132,7 +2143,12 @@ async function main() {
2132
2143
  await server.connect(transport);
2133
2144
  process.stderr.write(`agent-memory-mcp · storage: ${MEMORY_DIR}\n`);
2134
2145
  }
2135
- main().catch((err) => {
2136
- process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
2137
- process.exit(1);
2138
- });
2146
+ // Only auto-run main() when invoked directly. Importing this file
2147
+ // (e.g. from src/tui.tsx) should not trigger the dispatch.
2148
+ const isEntryPoint = process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url);
2149
+ if (isEntryPoint) {
2150
+ main().catch((err) => {
2151
+ process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
2152
+ process.exit(1);
2153
+ });
2154
+ }
package/dist/tui.js ADDED
@@ -0,0 +1,183 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * agent-memory-mcp · TUI
4
+ *
5
+ * The visual face of the project — Ink-based terminal UI for browsing,
6
+ * filtering, searching, and editing memories.
7
+ *
8
+ * Layout:
9
+ * ┌─ agent-memory ──────────────────────────────────────────┐
10
+ * │ [all] user feedback project reference · N memories │
11
+ * ├──────────────────────────────────────────────────────────┤
12
+ * │ memory list (scrolling) │
13
+ * │ ▶ highlighted name [type] · tags │
14
+ * │ description │
15
+ * ├──────────────────────────────────────────────────────────┤
16
+ * │ detail pane for highlighted memory │
17
+ * ├──────────────────────────────────────────────────────────┤
18
+ * │ key hints footer │
19
+ * └──────────────────────────────────────────────────────────┘
20
+ *
21
+ * Launched via `agent-memory ui`. Dynamic-imported from index.ts so
22
+ * Ink + React only load when the TUI is actually invoked.
23
+ */
24
+ import { Box, render, Text, useApp, useInput, useStdout } from "ink";
25
+ import TextInput from "ink-text-input";
26
+ import { spawnSync } from "node:child_process";
27
+ import { useEffect, useMemo, useState } from "react";
28
+ import Fuse from "fuse.js";
29
+ import { listMemoryFiles, MEMORY_DIR, memoryFilePath, readMemory, toolDeleteMemory, } from "./index.js";
30
+ const TYPE_FILTERS = ["all", "user", "feedback", "project", "reference"];
31
+ function loadAllMemories() {
32
+ return listMemoryFiles()
33
+ .map((n) => readMemory(n))
34
+ .filter((m) => m !== null)
35
+ .sort((a, b) => a.name.localeCompare(b.name));
36
+ }
37
+ function filterMemories(memories, typeFilter, query) {
38
+ let list = memories;
39
+ if (typeFilter !== "all")
40
+ list = list.filter((m) => m.type === typeFilter);
41
+ if (query.trim()) {
42
+ const fuse = new Fuse(list, {
43
+ includeScore: true,
44
+ threshold: 0.4,
45
+ ignoreLocation: true,
46
+ keys: [
47
+ { name: "name", weight: 3 },
48
+ { name: "description", weight: 2 },
49
+ { name: "body", weight: 1 },
50
+ ],
51
+ });
52
+ return fuse.search(query).map((r) => r.item);
53
+ }
54
+ return list;
55
+ }
56
+ const typeColor = {
57
+ user: "cyan",
58
+ feedback: "yellow",
59
+ project: "green",
60
+ reference: "magenta",
61
+ };
62
+ const App = () => {
63
+ const { exit } = useApp();
64
+ const { stdout } = useStdout();
65
+ const [state, setState] = useState({
66
+ memories: loadAllMemories(),
67
+ selected: 0,
68
+ typeFilter: "all",
69
+ searchMode: false,
70
+ searchQuery: "",
71
+ confirmDelete: null,
72
+ status: null,
73
+ });
74
+ const visible = useMemo(() => filterMemories(state.memories, state.typeFilter, state.searchQuery), [state.memories, state.typeFilter, state.searchQuery]);
75
+ // Keep the selection in range when filters shrink the list
76
+ useEffect(() => {
77
+ if (state.selected >= visible.length && visible.length > 0) {
78
+ setState((s) => ({ ...s, selected: Math.max(0, visible.length - 1) }));
79
+ }
80
+ else if (visible.length === 0 && state.selected !== 0) {
81
+ setState((s) => ({ ...s, selected: 0 }));
82
+ }
83
+ }, [visible.length, state.selected]);
84
+ const current = visible[state.selected];
85
+ const refresh = () => setState((s) => ({ ...s, memories: loadAllMemories(), status: "refreshed" }));
86
+ useInput((input, key) => {
87
+ if (state.searchMode)
88
+ return; // TextInput component handles search input
89
+ // Delete confirmation flow
90
+ if (state.confirmDelete) {
91
+ if (input === "y" || input === "Y") {
92
+ try {
93
+ toolDeleteMemory({ name: state.confirmDelete });
94
+ setState((s) => ({
95
+ ...s,
96
+ memories: loadAllMemories(),
97
+ confirmDelete: null,
98
+ status: `deleted "${state.confirmDelete}" (in .trash/)`,
99
+ }));
100
+ }
101
+ catch (err) {
102
+ setState((s) => ({
103
+ ...s,
104
+ confirmDelete: null,
105
+ status: `delete failed: ${err.message}`,
106
+ }));
107
+ }
108
+ }
109
+ else if (input === "n" || input === "N" || key.escape) {
110
+ setState((s) => ({ ...s, confirmDelete: null, status: "delete cancelled" }));
111
+ }
112
+ return;
113
+ }
114
+ if (input === "q" || (key.ctrl && input === "c")) {
115
+ exit();
116
+ return;
117
+ }
118
+ if (key.upArrow || input === "k") {
119
+ setState((s) => ({ ...s, selected: Math.max(0, s.selected - 1), status: null }));
120
+ return;
121
+ }
122
+ if (key.downArrow || input === "j") {
123
+ setState((s) => ({
124
+ ...s,
125
+ selected: Math.min(Math.max(0, visible.length - 1), s.selected + 1),
126
+ status: null,
127
+ }));
128
+ return;
129
+ }
130
+ // Type filter quick-keys
131
+ const typeKeyMap = {
132
+ "0": "all",
133
+ "1": "user",
134
+ "2": "feedback",
135
+ "3": "project",
136
+ "4": "reference",
137
+ };
138
+ if (typeKeyMap[input]) {
139
+ setState((s) => ({ ...s, typeFilter: typeKeyMap[input], selected: 0, status: null }));
140
+ return;
141
+ }
142
+ if (input === "/") {
143
+ setState((s) => ({ ...s, searchMode: true, status: null }));
144
+ return;
145
+ }
146
+ if (input === "r") {
147
+ refresh();
148
+ return;
149
+ }
150
+ if (input === "e" && current) {
151
+ const editor = process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
152
+ const fp = memoryFilePath(current.name);
153
+ // Suspend Ink rendering, spawn editor, then refresh after exit
154
+ stdout?.write("\x1bc"); // reset terminal to give the editor a clean canvas
155
+ const result = spawnSync(editor, [fp], { stdio: "inherit" });
156
+ setState((s) => ({
157
+ ...s,
158
+ memories: loadAllMemories(),
159
+ status: result.status === 0
160
+ ? `edited "${current.name}" in ${editor}`
161
+ : `editor exited with code ${result.status}`,
162
+ }));
163
+ return;
164
+ }
165
+ if (input === "d" && current) {
166
+ setState((s) => ({ ...s, confirmDelete: current.name, status: null }));
167
+ return;
168
+ }
169
+ });
170
+ const onSearchSubmit = () => setState((s) => ({ ...s, searchMode: false, selected: 0 }));
171
+ const onSearchChange = (value) => setState((s) => ({ ...s, searchQuery: value }));
172
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "agent-memory" }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), TYPE_FILTERS.map((t, i) => (_jsxs(Text, { bold: state.typeFilter === t, color: state.typeFilter === t ? "green" : "gray", children: [i > 0 ? " " : "", "[", i, "] ", t] }, t))), _jsxs(Text, { dimColor: true, children: [" · ", visible.length, " of ", state.memories.length, state.searchQuery && ` matching "${state.searchQuery}"`] })] }), state.searchMode && (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "/ " }), _jsx(TextInput, { value: state.searchQuery, onChange: onSearchChange, onSubmit: onSearchSubmit, placeholder: "fuzzy search \u00B7 enter to confirm" })] })), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [visible.length === 0 ? (_jsx(Text, { dimColor: true, children: "(no memories match)" })) : (visible.slice(0, 12).map((m, i) => {
173
+ const isSelected = i === state.selected;
174
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? "green" : undefined, bold: isSelected, children: [isSelected ? "▶ " : " ", m.name] }), _jsxs(Text, { color: typeColor[m.type] ?? "gray", children: [" [", m.type, "]"] }), m.tags.length > 0 ? (_jsxs(Text, { dimColor: true, children: [" · ", m.tags.join(" · ")] })) : null] }, m.name));
175
+ })), visible.length > 12 && (_jsxs(Text, { dimColor: true, children: [" ... +", visible.length - 12, " more (filter down with /)"] }))] }), current && (_jsxs(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: current.name }), _jsx(Text, { dimColor: true, children: current.description }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [current.body
176
+ .split("\n")
177
+ .slice(0, 10)
178
+ .map((line, i) => (_jsx(Text, { dimColor: line.startsWith("#") ? false : true, children: line || " " }, i))), current.body.split("\n").length > 10 && (_jsx(Text, { dimColor: true, children: "... (truncated \u00B7 press 'e' to open in editor)" }))] })] })), _jsxs(Box, { paddingX: 1, flexDirection: "column", children: [state.confirmDelete ? (_jsxs(Text, { color: "yellow", children: ["Delete \"", state.confirmDelete, "\"? (y/n) \u00B7 soft-delete, recoverable from .trash/"] })) : (_jsx(Text, { dimColor: true, children: "\u2191\u2193/jk navigate \u00B7 0-4 type filter \u00B7 / search \u00B7 e edit \u00B7 d delete \u00B7 r refresh \u00B7 q quit" })), state.status && _jsxs(Text, { color: "cyan", children: ["\u00B7 ", state.status] }), _jsxs(Text, { dimColor: true, children: ["storage: ", MEMORY_DIR] })] })] }));
179
+ };
180
+ export async function runTui() {
181
+ const { waitUntilExit } = render(_jsx(App, {}));
182
+ await waitUntilExit();
183
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xultrax-web/agent-memory-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "mcpName": "io.github.xultrax-web/agent-memory-mcp",
5
5
  "description": "Markdown memory for AI agents. Plain files you can read, edit, grep, and commit. The only MCP memory server that isn't a database.",
6
6
  "type": "module",
@@ -56,11 +56,15 @@
56
56
  "@modelcontextprotocol/sdk": "^1.0.4",
57
57
  "fuse.js": "^7.3.0",
58
58
  "gray-matter": "^4.0.3",
59
- "proper-lockfile": "^4.1.2"
59
+ "ink": "^7.0.3",
60
+ "ink-text-input": "^6.0.0",
61
+ "proper-lockfile": "^4.1.2",
62
+ "react": "^19.2.6"
60
63
  },
61
64
  "devDependencies": {
62
65
  "@types/node": "^22.10.2",
63
66
  "@types/proper-lockfile": "^4.1.4",
67
+ "@types/react": "^19.2.15",
64
68
  "prettier": "^3.8.3",
65
69
  "typescript": "^5.7.2",
66
70
  "vitest": "^4.1.7"