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

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
@@ -64,17 +64,15 @@ If you want vector similarity search, semantic recall, or auto-relation extracti
64
64
 
65
65
  ## Install
66
66
 
67
- ### Quick start (works today, no npm needed)
67
+ ### From npm (recommended)
68
68
 
69
69
  ```bash
70
- npx -y github:xultrax-web/agent-memory-mcp
70
+ npx -y @xultrax-web/agent-memory-mcp
71
71
  ```
72
72
 
73
- ### From npm (once published — landing v0.4.0)
73
+ ### Listed in the MCP Registry
74
74
 
75
- ```bash
76
- npx -y @xultrax-web/agent-memory-mcp
77
- ```
75
+ `io.github.xultrax-web/agent-memory-mcp` · browse at https://registry.modelcontextprotocol.io
78
76
 
79
77
  ### Build locally
80
78
 
@@ -103,7 +101,7 @@ Same JSON, slightly different paths per client.
103
101
  "mcpServers": {
104
102
  "agent-memory": {
105
103
  "command": "npx",
106
- "args": ["-y", "github:xultrax-web/agent-memory-mcp"]
104
+ "args": ["-y", "@xultrax-web/agent-memory-mcp"]
107
105
  }
108
106
  }
109
107
  }
@@ -117,7 +115,7 @@ Cline → MCP Servers → Add:
117
115
  {
118
116
  "agent-memory": {
119
117
  "command": "npx",
120
- "args": ["-y", "github:xultrax-web/agent-memory-mcp"]
118
+ "args": ["-y", "@xultrax-web/agent-memory-mcp"]
121
119
  }
122
120
  }
123
121
  ```
@@ -131,7 +129,7 @@ Cline → MCP Servers → Add:
131
129
  "mcpServers": {
132
130
  "agent-memory": {
133
131
  "command": "npx",
134
- "args": ["-y", "github:xultrax-web/agent-memory-mcp"]
132
+ "args": ["-y", "@xultrax-web/agent-memory-mcp"]
135
133
  }
136
134
  }
137
135
  }
@@ -140,7 +138,7 @@ Cline → MCP Servers → Add:
140
138
  > **Windows note:** if `npx` doesn't resolve cleanly, wrap with `cmd /c`:
141
139
  >
142
140
  > ```json
143
- > { "command": "cmd", "args": ["/c", "npx", "-y", "github:xultrax-web/agent-memory-mcp"] }
141
+ > { "command": "cmd", "args": ["/c", "npx", "-y", "@xultrax-web/agent-memory-mcp"] }
144
142
  > ```
145
143
 
146
144
  ### Continue.dev
@@ -155,7 +153,7 @@ Cline → MCP Servers → Add:
155
153
  "transport": {
156
154
  "type": "stdio",
157
155
  "command": "npx",
158
- "args": ["-y", "github:xultrax-web/agent-memory-mcp"]
156
+ "args": ["-y", "@xultrax-web/agent-memory-mcp"]
159
157
  }
160
158
  }
161
159
  ]
@@ -272,6 +270,7 @@ agent-memory sync init git@github.com:you/agent-memory.git # multi-machine se
272
270
  agent-memory sync push # commit + push local changes
273
271
  agent-memory sync pull # fast-forward from remote
274
272
  agent-memory sync status # local + ahead/behind state
273
+ agent-memory ui # launch the TUI (browse + edit interactively)
275
274
  ```
276
275
 
277
276
  ### Multi-machine memory (git sync)
@@ -429,25 +428,54 @@ This server is built to be used daily, not to demo well once.
429
428
  - Per-machine state (`.lock`, `.events.jsonl`, `.trash/`) auto-excluded from sync.
430
429
  - Default commit identity injected (`agent-memory@local`) so machines without `git config --global user.email` work without setup.
431
430
 
432
- **Landing in v0.10+:**
431
+ **Shipped in v0.10 · the visual identity (TUI):**
432
+
433
+ - **`agent-memory ui`** — Ink-based terminal UI for browsing, filtering, searching, and editing memories without leaving the terminal.
434
+ - Type-filter quick-keys (0-4 cycle through all/user/feedback/project/reference)
435
+ - Fuzzy live search with `/`
436
+ - `e` opens the highlighted memory in `$EDITOR` (vim/notepad/nano/whatever) — saves back to disk
437
+ - `d` soft-deletes with `y/n` confirmation
438
+ - Detail pane previews the body of the selected memory
439
+ - Color-coded by type, tag chips inline
440
+
441
+ **Landing in v0.11+:**
433
442
 
434
- - TUI / web UI for browsing + editing memories in a clean interface
435
443
  - Folder support (`.agent-memory/work/`, `.agent-memory/personal/`)
436
444
  - Memory packs for shareable curated bundles
445
+ - Web UI for browser-based memory browsing (companion to the TUI)
446
+ - Auto-context loading (LLM gets relevant memories transparently before each prompt)
437
447
 
438
448
  ---
439
449
 
440
450
  ## Roadmap
441
451
 
442
- - **v0.2** — MCP Resources support, Claude Code import script (`agent-memory import-claude-code`), CLI mode
443
- - **v0.3** — Atomic writes, soft delete, schema versioning, doctor command
444
- - **v0.4** — Structured event log, stats command, color output
445
- - **v0.5** Fuzzy search (Fuse.js), BM25 ranking, snippet highlighting
446
- - **v0.7** Comprehensive test suite, multi-client compatibility matrix
447
- - **v0.9** npm publish + MCP Registry submission
448
- - **v1.0** Public launch
449
-
450
- Beyond v1.0: sync backends (git remote, S3), web UI, team mode, browser extension, optional embeddings sidecar.
452
+ ### Released
453
+
454
+ | Version | Highlights |
455
+ | --------- | --------------------------------------------------------------------------------------------------- |
456
+ | v0.1 | Five-tool MVP, file storage, four-client config snippets |
457
+ | v0.2 | MCP Resources, Claude Code import (`agent-memory import-claude-code`), CLI mode, prettier baseline |
458
+ | v0.3 | Atomic writes, file locking, soft delete + `restore_memory`, `doctor` repair, schema versioning |
459
+ | v0.4 | Append-only event log (`.events.jsonl`), `stats` dashboard, `log_events` browser, color output |
460
+ | v0.5 | Fuzzy search via Fuse.js, relevance scoring, body-context snippets, `relevant_memories`, pagination |
461
+ | v0.6 | 25+ Vitest tests, GitHub Actions CI (Node 20/22/24 matrix), `COMPATIBILITY.md` |
462
+ | v0.7 | MCP Prompts (4 starter workflows), `verify_memory`, conflict detection on save |
463
+ | v0.8 | Tags, `[[wiki-links]]`, `find_backlinks`, `find_related` |
464
+ | v0.8.1 | Trusted Publishing live · tokenless OIDC publishes to npm + MCP Registry on git tag |
465
+ | **v0.9** | **`agent-memory sync` · multi-machine memory via git remote (init/push/pull/status/log)** |
466
+ | **v0.10** | **Ink-based TUI · `agent-memory ui` for visual browsing, search, and editing** |
467
+
468
+ ### Coming next
469
+
470
+ - Folder support inside the store (`.agent-memory/work/`, `.agent-memory/personal/`) for multi-context separation
471
+ - Auto-context loading — server hook that auto-fires `relevant_memories` before each LLM turn so context flows transparently
472
+ - Memory packs — export/import shareable `.tar.gz` bundles of curated memories
473
+ - Browser companion UI (`agent-memory web`) for non-terminal users
474
+ - TUI polish — file-watching for auto-refresh, inline editing, sync push/pull as keybindings
475
+
476
+ ### Beyond
477
+
478
+ Optional local-embeddings sidecar (transformers.js, no API), team mode with diff/merge, browser extension to capture from chatgpt.com / claude.ai → memory, mobile companion.
451
479
 
452
480
  Open an issue if you want one of these before I get to it.
453
481
 
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.1" }, { 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.1",
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"