nmem-cli 0.8.3__tar.gz → 0.8.8__tar.gz

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.
Files changed (25) hide show
  1. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/PKG-INFO +1 -1
  2. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/pyproject.toml +1 -1
  3. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/__init__.py +1 -1
  4. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/cli.py +15 -0
  5. nmem_cli-0.8.8/src/nmem_cli/kfs_cli.py +334 -0
  6. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/.gitignore +0 -0
  7. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/README.md +0 -0
  8. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/claude_paths.py +0 -0
  9. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/license_payload.py +0 -0
  10. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/py.typed +0 -0
  11. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/session_import.py +0 -0
  12. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/__init__.py +0 -0
  13. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/__main__.py +0 -0
  14. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/api_client.py +0 -0
  15. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/app.py +0 -0
  16. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/__init__.py +0 -0
  17. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/dashboard.py +0 -0
  18. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/graph.py +0 -0
  19. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/help.py +0 -0
  20. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/memories.py +0 -0
  21. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
  22. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/settings.py +0 -0
  23. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
  24. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/screens/threads.py +0 -0
  25. {nmem_cli-0.8.3 → nmem_cli-0.8.8}/src/nmem_cli/tui/widgets/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nmem-cli
3
- Version: 0.8.3
3
+ Version: 0.8.8
4
4
  Summary: CLI and TUI for Nowledge Mem - AI memory management
5
5
  Project-URL: Homepage, https://mem.nowledge.co/
6
6
  Project-URL: Repository, https://github.com/nowledge-co/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nmem-cli"
3
- version = "0.8.3"
3
+ version = "0.8.8"
4
4
  description = "CLI and TUI for Nowledge Mem - AI memory management"
5
5
  authors = [
6
6
  {name = "Nowledge Labs"}
@@ -20,7 +20,7 @@ Environment (overrides config file):
20
20
  NMEM_API_KEY Optional API key (Bearer auth)
21
21
  """
22
22
 
23
- __version__ = "0.8.3"
23
+ __version__ = "0.8.8"
24
24
  __author__ = "Nowledge Labs"
25
25
 
26
26
  from .cli import main
@@ -33,6 +33,7 @@ from rich.prompt import Confirm, Prompt
33
33
  from rich.table import Table
34
34
 
35
35
  from . import __version__
36
+ from .kfs_cli import handle_kfs_command, register_kfs_parser
36
37
  from .license_payload import decode_license_email
37
38
  from .session_import import SessionImportError, discover_sessions, parse_session
38
39
 
@@ -9214,6 +9215,9 @@ PRIORITY
9214
9215
  # stats
9215
9216
  subparsers.add_parser("stats", help="Show statistics")
9216
9217
 
9218
+ # Knowledge Filesystem
9219
+ register_kfs_parser(subparsers)
9220
+
9217
9221
  # export / import
9218
9222
  export_parser = subparsers.add_parser("export", help="Export data to folder/zip (experimental)")
9219
9223
  export_parser.add_argument("path", help="Target export directory or .zip path")
@@ -10604,6 +10608,17 @@ def main() -> int:
10604
10608
  return 1
10605
10609
  elif cmd == "stats":
10606
10610
  cmd_stats()
10611
+ elif cmd == "fs":
10612
+ return handle_kfs_command(
10613
+ args,
10614
+ api_get=api_get,
10615
+ api_post=api_post,
10616
+ output_json=output_json,
10617
+ is_json_mode=is_json_mode,
10618
+ console=console,
10619
+ print_success=print_success,
10620
+ print_error=print_error,
10621
+ )
10607
10622
  elif cmd == "export":
10608
10623
  cmd_export_data(
10609
10624
  args.path,
@@ -0,0 +1,334 @@
1
+ """Nowledge FS commands for ``nmem fs``.
2
+
3
+ The CLI module is intentionally separate from ``cli.py`` because the main
4
+ entrypoint is already oversized. This file owns only parser registration,
5
+ small output shaping, and dispatch to existing ``/fs/*`` endpoints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Any, Callable
15
+
16
+ ApiGet = Callable[[str, dict[str, Any] | None], dict[str, Any]]
17
+ ApiPost = Callable[[str, dict[str, Any]], dict[str, Any]]
18
+
19
+
20
+ def register_kfs_parser(subparsers: argparse._SubParsersAction) -> None:
21
+ fs = subparsers.add_parser(
22
+ "fs",
23
+ help="Nowledge FS for agents and scripts",
24
+ description=(
25
+ "Browse Mem through Nowledge FS. Paths are graph projections, "
26
+ "not files on disk: use ls, cat, stat, find, grep, and recall."
27
+ ),
28
+ epilog="""examples:
29
+ nmem fs ls /
30
+ nmem fs cat /wiki/entities/PostgreSQL.entity.md
31
+ nmem fs cat /sources/report-abc123.pdf --line 120 --lines 40
32
+ nmem fs find /memories --label decisions --since 2026-01-01
33
+ nmem fs grep "JWT rotation" /memories
34
+ nmem fs grep "JWT rotation" /threads --jsonl
35
+ nmem fs recall "session token strategy" --in /memories
36
+ cat note.md | nmem fs write /memories/by-id/mem-abc.memory.md --stdin""",
37
+ formatter_class=argparse.RawDescriptionHelpFormatter,
38
+ )
39
+ fs_subs = fs.add_subparsers(dest="fs_action")
40
+
41
+ ls = fs_subs.add_parser("ls", help="List a Nowledge FS directory")
42
+ _add_json_flag(ls)
43
+ ls.add_argument("path", nargs="?", default="/")
44
+ ls.add_argument("-n", "--limit", type=int, default=200)
45
+ ls.add_argument("--cursor", help="Opaque pagination cursor")
46
+ ls.add_argument("-l", "--long", action="store_true", help="Show paths and hints")
47
+
48
+ cat = fs_subs.add_parser("cat", help="Read a Nowledge FS file")
49
+ _add_json_flag(cat)
50
+ cat.add_argument("path")
51
+ cat.add_argument("--line", type=int, help="One-based line to start reading from")
52
+ cat.add_argument("--lines", type=int, help="Number of lines to print")
53
+ cat.add_argument("--fragment", help="Fragment selector, for future format-aware reads")
54
+ cat.add_argument("--raw", action="store_true", help="Request raw source bytes when supported")
55
+ cat.add_argument("--frontmatter", action="store_true", help="Print frontmatter before body")
56
+
57
+ stat = fs_subs.add_parser("stat", help="Show metadata for a Nowledge FS path")
58
+ _add_json_flag(stat)
59
+ stat.add_argument("path")
60
+
61
+ find = fs_subs.add_parser("find", help="Structural search over Nowledge FS paths")
62
+ _add_json_flag(find)
63
+ find.add_argument("path", nargs="?", default="/memories")
64
+ find.add_argument("--type", choices=["memory", "crystal"], help="Filter by file type")
65
+ find.add_argument("--label", help="Filter by label")
66
+ find.add_argument("--since", help="Lower recorded-time bound")
67
+ find.add_argument("--until", help="Upper recorded-time bound")
68
+ find.add_argument("--mentions", help="Filter memories mentioning this entity")
69
+ find.add_argument("-n", "--limit", type=int, default=50)
70
+ find.add_argument("--cursor", help="Opaque pagination cursor")
71
+ find.add_argument("--jsonl", action="store_true", help="Print one JSON path hit per line")
72
+ find.add_argument("--paths", action="store_true", help="Print paths only")
73
+
74
+ grep = fs_subs.add_parser("grep", help="Literal text search")
75
+ _add_json_flag(grep)
76
+ grep.add_argument("query")
77
+ grep.add_argument("path", nargs="?", default="/memories")
78
+ grep.add_argument("-n", "--limit", type=int, default=50)
79
+ grep.add_argument("--cursor", help="Opaque pagination cursor")
80
+ grep.add_argument("--jsonl", action="store_true", help="Print one JSON match per line")
81
+ grep.add_argument("--paths", action="store_true", help="Print unique matched paths only")
82
+
83
+ recall = fs_subs.add_parser("recall", help="Semantic memory search returning Nowledge FS paths")
84
+ _add_json_flag(recall)
85
+ recall.add_argument("query")
86
+ recall.add_argument("-i", "--in", dest="path", default="/memories")
87
+ recall.add_argument("-k", type=int, default=20, help="Number of results")
88
+ recall.add_argument("--jsonl", action="store_true", help="Print one JSON path hit per line")
89
+ recall.add_argument("--paths", action="store_true", help="Print paths only")
90
+
91
+ write = fs_subs.add_parser("write", help="Write a canonical Nowledge FS path")
92
+ _add_json_flag(write)
93
+ write.add_argument("path")
94
+ body = write.add_mutually_exclusive_group(required=True)
95
+ body.add_argument("--body", help="Markdown body to write")
96
+ body.add_argument("--file", type=Path, help="Read markdown body from a local file")
97
+ body.add_argument("--stdin", action="store_true", help="Read markdown body from stdin")
98
+ write.add_argument("--frontmatter-json", help="JSON object merged into frontmatter")
99
+ write.add_argument("--record-version", help="Optional optimistic record version")
100
+
101
+ rm = fs_subs.add_parser("rm", help="Delete a canonical Nowledge FS path")
102
+ _add_json_flag(rm)
103
+ rm.add_argument("path")
104
+ rm.add_argument("-f", "--force", action="store_true", help="Skip confirmation")
105
+
106
+
107
+ def _add_json_flag(parser: argparse.ArgumentParser) -> None:
108
+ parser.add_argument(
109
+ "--json",
110
+ action="store_true",
111
+ default=argparse.SUPPRESS,
112
+ help="JSON output",
113
+ )
114
+
115
+
116
+ def handle_kfs_command(
117
+ args: argparse.Namespace,
118
+ *,
119
+ api_get: ApiGet,
120
+ api_post: ApiPost,
121
+ output_json: Callable[[Any], None],
122
+ is_json_mode: Callable[[], bool],
123
+ console: Any,
124
+ print_success: Callable[[str, str | None], None],
125
+ print_error: Callable[[str, str, str | None], None],
126
+ ) -> int:
127
+ action = getattr(args, "fs_action", None)
128
+ if action is None:
129
+ console.print("[bold]nmem fs[/bold] verbs: ls, cat, stat, find, grep, recall, write, rm")
130
+ return 0
131
+
132
+ if action == "ls":
133
+ data = api_get("/fs/ls", _params(path=args.path, limit=args.limit, cursor=args.cursor))
134
+ if is_json_mode():
135
+ output_json(data)
136
+ else:
137
+ _print_ls(console, data, long=getattr(args, "long", False))
138
+ return 0
139
+
140
+ if action == "cat":
141
+ data = api_get(
142
+ "/fs/cat",
143
+ _params(
144
+ path=args.path,
145
+ line=getattr(args, "line", None),
146
+ lines=getattr(args, "lines", None),
147
+ fragment=args.fragment,
148
+ raw=args.raw or None,
149
+ ),
150
+ )
151
+ if is_json_mode():
152
+ output_json(data)
153
+ else:
154
+ if getattr(args, "frontmatter", False) and data.get("frontmatter"):
155
+ console.print(json.dumps(data["frontmatter"], indent=2, ensure_ascii=False))
156
+ console.print(str(data.get("body") or ""), end="" if str(data.get("body") or "").endswith("\n") else "\n")
157
+ return 0
158
+
159
+ if action == "stat":
160
+ data = api_get("/fs/stat", {"path": args.path})
161
+ if is_json_mode():
162
+ output_json(data)
163
+ else:
164
+ _print_stat(console, data)
165
+ return 0
166
+
167
+ if action == "find":
168
+ data = api_get(
169
+ "/fs/find",
170
+ _params(
171
+ path=args.path,
172
+ type=getattr(args, "type", None),
173
+ label=getattr(args, "label", None),
174
+ since=getattr(args, "since", None),
175
+ until=getattr(args, "until", None),
176
+ mentions=getattr(args, "mentions", None),
177
+ limit=getattr(args, "limit", 50),
178
+ cursor=getattr(args, "cursor", None),
179
+ ),
180
+ )
181
+ return _print_hits(console, output_json, is_json_mode, data, args=args)
182
+
183
+ if action == "grep":
184
+ data = api_get(
185
+ "/fs/grep",
186
+ _params(path=args.path, q=args.query, limit=args.limit, cursor=args.cursor),
187
+ )
188
+ if is_json_mode():
189
+ output_json(data)
190
+ elif getattr(args, "jsonl", False):
191
+ _print_jsonl(data.get("matches", []) or [])
192
+ elif getattr(args, "paths", False):
193
+ _print_unique_paths(data.get("matches", []) or [])
194
+ else:
195
+ for match in data.get("matches", []) or []:
196
+ console.print(f"{match.get('path')}:{match.get('line', 1)}:{match.get('match', '')}")
197
+ _print_next_cursor(console, data)
198
+ return 0
199
+
200
+ if action == "recall":
201
+ data = api_get("/fs/recall", _params(query=args.query, path=args.path, k=args.k))
202
+ return _print_hits(console, output_json, is_json_mode, data, args=args)
203
+
204
+ if action == "write":
205
+ frontmatter = _parse_frontmatter(getattr(args, "frontmatter_json", None))
206
+ body = _read_body(args)
207
+ data = api_post(
208
+ "/fs/write",
209
+ {
210
+ "path": args.path,
211
+ "body": body,
212
+ "frontmatter": frontmatter,
213
+ "record_version": getattr(args, "record_version", None),
214
+ },
215
+ )
216
+ if is_json_mode():
217
+ output_json(data)
218
+ else:
219
+ print_success("Wrote Nowledge FS path", args.path)
220
+ return 0
221
+
222
+ if action == "rm":
223
+ if not getattr(args, "force", False) and not is_json_mode():
224
+ print_error(
225
+ "Confirmation Required",
226
+ "Pass --force to delete through Nowledge FS.",
227
+ "Only canonical writable paths can be deleted.",
228
+ )
229
+ return 1
230
+ data = api_post("/fs/delete", {"path": args.path})
231
+ if is_json_mode():
232
+ output_json(data)
233
+ else:
234
+ print_success("Deleted Nowledge FS path", args.path)
235
+ return 0
236
+
237
+ print_error("Unknown fs verb", str(action), "Run nmem fs --help.")
238
+ return 1
239
+
240
+
241
+ def _params(**values: Any) -> dict[str, Any]:
242
+ return {key: value for key, value in values.items() if value is not None}
243
+
244
+
245
+ def _print_ls(console: Any, data: dict[str, Any], *, long: bool) -> None:
246
+ for entry in data.get("entries", []) or []:
247
+ kind = "dir" if entry.get("kind") in {"directory", "dir"} else "file"
248
+ name = entry.get("name") or entry.get("path")
249
+ if long:
250
+ hint = entry.get("hint") or entry.get("updated_at") or ""
251
+ console.print(f"{kind:4} {entry.get('path', name)} {hint}".rstrip())
252
+ else:
253
+ suffix = "/" if kind == "dir" else ""
254
+ console.print(f"{name}{suffix}")
255
+ _print_next_cursor(console, data)
256
+
257
+
258
+ def _print_stat(console: Any, data: dict[str, Any]) -> None:
259
+ for key in ("path", "kind", "type", "id", "ext", "size_bytes", "created_at", "updated_at"):
260
+ if key in data and data.get(key) is not None:
261
+ console.print(f"{key}: {data[key]}")
262
+ fm = data.get("frontmatter")
263
+ if isinstance(fm, dict) and fm:
264
+ console.print("frontmatter:")
265
+ for key, value in fm.items():
266
+ console.print(f" {key}: {value}")
267
+
268
+
269
+ def _print_hits(
270
+ console: Any,
271
+ output_json: Callable[[Any], None],
272
+ is_json_mode: Callable[[], bool],
273
+ data: dict[str, Any],
274
+ *,
275
+ args: argparse.Namespace,
276
+ ) -> int:
277
+ if is_json_mode():
278
+ output_json(data)
279
+ elif getattr(args, "jsonl", False):
280
+ _print_jsonl(data.get("paths") or data.get("hits") or [])
281
+ elif getattr(args, "paths", False):
282
+ _print_unique_paths(data.get("paths") or data.get("hits") or [])
283
+ else:
284
+ for hit in data.get("paths") or data.get("hits") or []:
285
+ line = str(hit.get("path") or "")
286
+ if hit.get("score") is not None:
287
+ line += f" score={hit.get('score')}"
288
+ console.print(line)
289
+ excerpt = hit.get("excerpt") or hit.get("snippet")
290
+ if excerpt:
291
+ console.print(f" {excerpt}")
292
+ _print_next_cursor(console, data)
293
+ return 0
294
+
295
+
296
+ def _print_jsonl(rows: list[dict[str, Any]]) -> None:
297
+ for row in rows:
298
+ sys.stdout.write(json.dumps(row, ensure_ascii=False, separators=(",", ":")))
299
+ sys.stdout.write("\n")
300
+
301
+
302
+ def _print_unique_paths(rows: list[dict[str, Any]]) -> None:
303
+ seen: set[str] = set()
304
+ for row in rows:
305
+ path = str(row.get("path") or "")
306
+ if not path or path in seen:
307
+ continue
308
+ seen.add(path)
309
+ sys.stdout.write(path)
310
+ sys.stdout.write("\n")
311
+
312
+
313
+ def _print_next_cursor(console: Any, data: dict[str, Any]) -> None:
314
+ cursor = data.get("next_cursor") or data.get("nextCursor")
315
+ if cursor:
316
+ console.print(f"[dim]next cursor: {cursor}[/dim]")
317
+
318
+
319
+ def _read_body(args: argparse.Namespace) -> str:
320
+ if getattr(args, "stdin", False):
321
+ return sys.stdin.read()
322
+ file_path = getattr(args, "file", None)
323
+ if file_path:
324
+ return file_path.read_text(encoding="utf-8")
325
+ return str(getattr(args, "body", "") or "")
326
+
327
+
328
+ def _parse_frontmatter(raw: str | None) -> dict[str, Any] | None:
329
+ if not raw:
330
+ return None
331
+ parsed = json.loads(raw)
332
+ if not isinstance(parsed, dict):
333
+ raise SystemExit("--frontmatter-json must be a JSON object")
334
+ return parsed
File without changes
File without changes
File without changes