nmem-cli 0.7.9__tar.gz → 0.8.1__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 (23) hide show
  1. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/PKG-INFO +16 -1
  2. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/README.md +15 -0
  3. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/pyproject.toml +1 -1
  4. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/__init__.py +1 -1
  5. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/cli.py +495 -11
  6. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/session_import.py +14 -3
  7. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/__init__.py +14 -7
  8. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/__main__.py +1 -1
  9. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/.gitignore +0 -0
  10. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/license_payload.py +0 -0
  11. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/py.typed +0 -0
  12. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/api_client.py +0 -0
  13. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/app.py +0 -0
  14. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/__init__.py +0 -0
  15. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/dashboard.py +0 -0
  16. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/graph.py +0 -0
  17. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/help.py +0 -0
  18. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/memories.py +0 -0
  19. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
  20. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/settings.py +0 -0
  21. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
  22. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/src/nmem_cli/tui/screens/threads.py +0 -0
  23. {nmem_cli-0.7.9 → nmem_cli-0.8.1}/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.7.9
3
+ Version: 0.8.1
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/
@@ -180,6 +180,21 @@ Use environment variables when you want a temporary override for the current she
180
180
 
181
181
  `nmem config client ...` controls how this machine connects outward to Mem. It is separate from `nmem config access ...`, which controls how a Mem server is exposed to other devices on your network or through Access Anywhere.
182
182
 
183
+ ## MCP Host Configuration
184
+
185
+ Direct HTTP MCP clients do not read `~/.nowledge-mem/config.json` by themselves. The host owns its MCP transport, so remote Mem needs headers in that host's MCP settings.
186
+
187
+ Generate the right snippet from the client config you already saved:
188
+
189
+ ```bash
190
+ nmem config mcp show --host codex
191
+ nmem config mcp show --host gemini-cli
192
+ nmem config mcp show --host cursor
193
+ nmem config mcp show --host claude-desktop
194
+ ```
195
+
196
+ For a fixed Mem space, add `--space "Research Agent"`. The generated snippet includes your API key when one is configured, so paste it only into the target host's private MCP config.
197
+
183
198
  ## Environment Variables
184
199
 
185
200
  | Variable | Description | Default |
@@ -147,6 +147,21 @@ Use environment variables when you want a temporary override for the current she
147
147
 
148
148
  `nmem config client ...` controls how this machine connects outward to Mem. It is separate from `nmem config access ...`, which controls how a Mem server is exposed to other devices on your network or through Access Anywhere.
149
149
 
150
+ ## MCP Host Configuration
151
+
152
+ Direct HTTP MCP clients do not read `~/.nowledge-mem/config.json` by themselves. The host owns its MCP transport, so remote Mem needs headers in that host's MCP settings.
153
+
154
+ Generate the right snippet from the client config you already saved:
155
+
156
+ ```bash
157
+ nmem config mcp show --host codex
158
+ nmem config mcp show --host gemini-cli
159
+ nmem config mcp show --host cursor
160
+ nmem config mcp show --host claude-desktop
161
+ ```
162
+
163
+ For a fixed Mem space, add `--space "Research Agent"`. The generated snippet includes your API key when one is configured, so paste it only into the target host's private MCP config.
164
+
150
165
  ## Environment Variables
151
166
 
152
167
  | Variable | Description | Default |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nmem-cli"
3
- version = "0.7.9"
3
+ version = "0.8.1"
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.7.9"
23
+ __version__ = "0.8.1"
24
24
  __author__ = "Nowledge Labs"
25
25
 
26
26
  from .cli import main
@@ -1378,6 +1378,88 @@ def _is_wsl_environment() -> bool:
1378
1378
  return False
1379
1379
 
1380
1380
 
1381
+ class _CliInstallContext(NamedTuple):
1382
+ source: str
1383
+ command_path: str | None
1384
+ python_executable: str
1385
+ package_path: str
1386
+
1387
+
1388
+ def _detect_cli_install_context() -> _CliInstallContext:
1389
+ command_path = shutil.which("nmem")
1390
+ python_executable = str(Path(sys.executable).resolve())
1391
+ package_path = str(Path(__file__).resolve())
1392
+ haystack = " ".join(
1393
+ item.lower()
1394
+ for item in [command_path or "", python_executable, package_path]
1395
+ )
1396
+
1397
+ if "python-standalone" in haystack or "nowledge mem.app" in haystack:
1398
+ source = "desktop-bundled"
1399
+ elif "/usr/lib/nowledge-mem" in haystack or "/usr/lib/nowledge mem" in haystack:
1400
+ source = "desktop-bundled"
1401
+ elif "\\nowledge mem\\" in haystack or "/.local/share/nowledge-mem/" in haystack:
1402
+ source = "desktop-bundled"
1403
+ elif "pipx/venvs/nmem-cli" in haystack or ".local/pipx/venvs/nmem-cli" in haystack:
1404
+ source = "pipx"
1405
+ elif "/uv/tools/nmem-cli" in haystack or "\\uv\\tools\\nmem-cli" in haystack:
1406
+ source = "uv"
1407
+ elif "site-packages/nmem_cli" in haystack:
1408
+ source = "python-package"
1409
+ else:
1410
+ source = "unknown"
1411
+
1412
+ return _CliInstallContext(
1413
+ source=source,
1414
+ command_path=command_path,
1415
+ python_executable=python_executable,
1416
+ package_path=package_path,
1417
+ )
1418
+
1419
+
1420
+ def _format_cli_upgrade_hint(*, context: _CliInstallContext, server_newer: bool) -> str:
1421
+ command_detail = (
1422
+ f" first on PATH ({context.command_path})" if context.command_path else ""
1423
+ )
1424
+ if server_newer:
1425
+ if context.source == "desktop-bundled":
1426
+ return (
1427
+ f"The CLI{command_detail} is older than the connected server. "
1428
+ "In the desktop app, open Settings > Preferences > Developer Tools > "
1429
+ "Install CLI, then restart the terminal. If this terminal is using a "
1430
+ "PyPI/pipx install instead, run "
1431
+ "`python -m pip install --upgrade nmem-cli` or `pipx upgrade nmem-cli`."
1432
+ )
1433
+ if context.source == "pipx":
1434
+ return (
1435
+ f"The CLI{command_detail} is older than the connected server. "
1436
+ "Run `pipx upgrade nmem-cli`, or use "
1437
+ "`uvx --from nmem-cli nmem status` for a fresh one-shot CLI."
1438
+ )
1439
+ if context.source == "uv":
1440
+ return (
1441
+ f"The CLI{command_detail} is older than the connected server. "
1442
+ "Use `uvx --from nmem-cli nmem status` for the latest published CLI, "
1443
+ "or reinstall the uv tool."
1444
+ )
1445
+ if context.source == "python-package":
1446
+ return (
1447
+ f"The CLI{command_detail} is older than the connected server. "
1448
+ "Run `python -m pip install --upgrade nmem-cli`, or "
1449
+ "`uvx --from nmem-cli nmem status` without installing."
1450
+ )
1451
+ return (
1452
+ f"The CLI{command_detail} is older than the connected server. "
1453
+ "Upgrade the `nmem-cli` package, or reinstall the desktop CLI from "
1454
+ "Settings > Preferences > Developer Tools."
1455
+ )
1456
+
1457
+ return (
1458
+ "The CLI is newer than the connected server. Update or restart "
1459
+ "Nowledge Mem, or point NMEM_API_URL at the server you intended to use."
1460
+ )
1461
+
1462
+
1381
1463
  def _status_version_warning(
1382
1464
  *,
1383
1465
  cli_version: str,
@@ -1389,21 +1471,26 @@ def _status_version_warning(
1389
1471
  if not normalized_server or normalized_server == normalized_cli:
1390
1472
  return None
1391
1473
 
1392
- message = (
1393
- f"CLI is v{normalized_cli}, but the server at {api_url} reports v{normalized_server}."
1474
+ message = "CLI is v{}, but the server at {} reports v{}.".format(
1475
+ normalized_cli,
1476
+ api_url,
1477
+ normalized_server,
1394
1478
  )
1395
1479
 
1396
1480
  parsed = urlsplit(api_url)
1397
1481
  if _is_wsl_environment() and _is_loopback_hostname(parsed.hostname):
1398
1482
  return (
1399
1483
  message,
1400
- "WSL localhost may be reaching the Windows desktop app instead of the Linux service. "
1401
- "If you expected the WSL server, stop the Windows app or point NMEM_API_URL at the Linux service explicitly.",
1484
+ "WSL localhost may be reaching the Windows desktop app instead of "
1485
+ "the Linux service. If you expected the WSL server, stop the "
1486
+ "Windows app or point NMEM_API_URL at the Linux service explicitly.",
1402
1487
  )
1403
1488
 
1489
+ context = _detect_cli_install_context()
1490
+ server_newer = _compare_versions(normalized_cli, normalized_server)
1404
1491
  return (
1405
1492
  message,
1406
- "Upgrade or restart the server process you are connected to, or point NMEM_API_URL at the intended server.",
1493
+ _format_cli_upgrade_hint(context=context, server_newer=server_newer),
1407
1494
  )
1408
1495
 
1409
1496
 
@@ -1487,6 +1574,18 @@ def cmd_status() -> None:
1487
1574
  except Exception:
1488
1575
  search_index = None
1489
1576
 
1577
+ version_warning = _status_version_warning(
1578
+ cli_version=result["cli_version"],
1579
+ server_version=result["server_version"],
1580
+ api_url=result["api_url"],
1581
+ )
1582
+ if version_warning:
1583
+ warning_message, warning_hint = version_warning
1584
+ result["version_mismatch"] = {
1585
+ "message": warning_message,
1586
+ "hint": warning_hint,
1587
+ }
1588
+
1490
1589
  if is_json_mode():
1491
1590
  if agent_data:
1492
1591
  result["agent"] = {
@@ -1535,11 +1634,6 @@ def cmd_status() -> None:
1535
1634
  console.print(f" search {state_label}")
1536
1635
  if search_index.get("message"):
1537
1636
  console.print(f" [dim]{search_index['message']}[/dim]")
1538
- version_warning = _status_version_warning(
1539
- cli_version=result["cli_version"],
1540
- server_version=result["server_version"],
1541
- api_url=result["api_url"],
1542
- )
1543
1637
  if version_warning:
1544
1638
  warning_message, warning_hint = version_warning
1545
1639
  console.print(f" [bold yellow]! Version mismatch[/bold yellow] {warning_message}")
@@ -2082,6 +2176,147 @@ def cmd_communities_detect(resolution: float = 1.0) -> None:
2082
2176
  console.print(f" [dim]{data['message']}[/dim]")
2083
2177
 
2084
2178
 
2179
+ # ═══════════════════════════════════════════════════════════════════════════════
2180
+ # Wiki Commands
2181
+ # ═══════════════════════════════════════════════════════════════════════════════
2182
+
2183
+
2184
+ def cmd_wiki_page(kind: str, id_or_name: str) -> None:
2185
+ """Render one Library wiki page as markdown."""
2186
+ kind_norm = (kind or "").strip().lower()
2187
+ if kind_norm not in {"entity", "crystal", "topic"}:
2188
+ print_error(
2189
+ "Bad kind",
2190
+ f"kind must be one of 'entity', 'crystal', 'topic'; got {kind!r}",
2191
+ )
2192
+ sys.exit(2)
2193
+ encoded = quote(id_or_name, safe="")
2194
+ url = f"{get_api_url()}/library/wiki-page/{kind_norm}/{encoded}"
2195
+ try:
2196
+ response = api_request("GET", url, timeout=30.0)
2197
+ except httpx.ConnectError:
2198
+ if is_json_mode():
2199
+ output_json({
2200
+ "error": "connection_failed",
2201
+ "message": f"Cannot connect to {get_api_url()}",
2202
+ })
2203
+ else:
2204
+ _print_connection_error()
2205
+ sys.exit(1)
2206
+ if response.status_code == 404:
2207
+ if is_json_mode():
2208
+ output_json({
2209
+ "error": "not_found", "kind": kind_norm, "id_or_name": id_or_name,
2210
+ })
2211
+ else:
2212
+ console.print(f"[red]Not found:[/red] {kind_norm} {id_or_name}")
2213
+ sys.exit(1)
2214
+ response.raise_for_status()
2215
+ text = response.text
2216
+ if is_json_mode():
2217
+ output_json({
2218
+ "kind": kind_norm,
2219
+ "id_or_name": id_or_name,
2220
+ "markdown": text,
2221
+ })
2222
+ else:
2223
+ console.print(Markdown(text))
2224
+
2225
+
2226
+ def cmd_wiki_export(output_dir: Optional[str] = None) -> None:
2227
+ """Build the wiki ZIP and save it locally."""
2228
+ url = f"{get_api_url()}/library/wiki-export"
2229
+ try:
2230
+ if not is_json_mode():
2231
+ with Progress(
2232
+ SpinnerColumn(),
2233
+ TextColumn("[cyan]Building wiki ZIP...[/cyan]"),
2234
+ console=console,
2235
+ transient=True,
2236
+ ) as p:
2237
+ p.add_task("", total=None)
2238
+ response = api_request("GET", url, timeout=300.0)
2239
+ else:
2240
+ response = api_request("GET", url, timeout=300.0)
2241
+ except httpx.ConnectError:
2242
+ if is_json_mode():
2243
+ output_json({
2244
+ "error": "connection_failed",
2245
+ "message": f"Cannot connect to {get_api_url()}",
2246
+ })
2247
+ else:
2248
+ _print_connection_error()
2249
+ sys.exit(1)
2250
+ response.raise_for_status()
2251
+ cd = response.headers.get("content-disposition", "")
2252
+ match = re.search(r'filename="([^"]+)"', cd)
2253
+ filename = match.group(1) if match else "nowledge-mem-wiki.zip"
2254
+ target_dir = Path(output_dir).expanduser() if output_dir else Path.cwd()
2255
+ target_dir.mkdir(parents=True, exist_ok=True)
2256
+ target = target_dir / filename
2257
+ target.write_bytes(response.content)
2258
+ if is_json_mode():
2259
+ output_json({
2260
+ "saved_to": str(target),
2261
+ "size_bytes": len(response.content),
2262
+ })
2263
+ else:
2264
+ console.print(
2265
+ f"[green]Saved[/green] {len(response.content):,} bytes to [bold]{target}[/bold]"
2266
+ )
2267
+ console.print(
2268
+ "[dim]Open the folder in Obsidian, Logseq, or any markdown reader; "
2269
+ "wikilinks resolve in all of them.[/dim]"
2270
+ )
2271
+
2272
+
2273
+ def cmd_wiki_topics(limit: int = 20) -> None:
2274
+ """List wiki topics (community clusters) with their counts."""
2275
+ if not is_json_mode():
2276
+ with Progress(
2277
+ SpinnerColumn(),
2278
+ TextColumn("[cyan]Loading topics...[/cyan]"),
2279
+ console=console,
2280
+ transient=True,
2281
+ ) as p:
2282
+ p.add_task("", total=None)
2283
+ data = api_get(
2284
+ "/library/wiki-index",
2285
+ params={"community_limit": limit, "top_per_community": 5},
2286
+ )
2287
+ else:
2288
+ data = api_get(
2289
+ "/library/wiki-index",
2290
+ params={"community_limit": limit, "top_per_community": 5},
2291
+ )
2292
+ if is_json_mode():
2293
+ output_json(data)
2294
+ return
2295
+ communities = data.get("communities", [])
2296
+ console.print()
2297
+ if not communities:
2298
+ console.print(
2299
+ "[dim]No topics yet. Run community detection from the Graph view "
2300
+ "(or `nmem c detect`) to populate them.[/dim]"
2301
+ )
2302
+ console.print()
2303
+ return
2304
+ console.print(f"[bold]Wiki topics[/bold] [dim]({len(communities)} found)[/dim]")
2305
+ console.print()
2306
+ for c in communities:
2307
+ name = c.get("name", "Unnamed")
2308
+ members = c.get("member_count", 0)
2309
+ ent = len(c.get("top_entities", []))
2310
+ cry = len(c.get("top_crystals", []))
2311
+ console.print(
2312
+ f" [bold cyan]{name}[/bold cyan] "
2313
+ f"[dim]({members} members · {cry} crystals · {ent} entities)[/dim]"
2314
+ )
2315
+ cid = c.get("community_id", "?")
2316
+ console.print(f" [dim]nmem wiki page topic {cid}[/dim]")
2317
+ console.print()
2318
+
2319
+
2085
2320
  # ═══════════════════════════════════════════════════════════════════════════════
2086
2321
  # Graph Commands
2087
2322
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -4643,6 +4878,7 @@ def cmd_threads_save(
4643
4878
  {
4644
4879
  "messages": thread_data["messages"],
4645
4880
  "deduplicate": True,
4881
+ **({"space_id": space_id} if space_id else {}),
4646
4882
  },
4647
4883
  )
4648
4884
  results.append(
@@ -8719,6 +8955,151 @@ def cmd_config_client_clear(key: str | None = None) -> None:
8719
8955
  )
8720
8956
 
8721
8957
 
8958
+ _MCP_HOST_APPS = {
8959
+ "codex": "Codex",
8960
+ "gemini-cli": "Gemini CLI",
8961
+ "cursor": "Cursor",
8962
+ "claude-desktop": "Claude",
8963
+ }
8964
+
8965
+
8966
+ def _mcp_endpoint_url(api_url: str) -> str:
8967
+ return f"{api_url.rstrip('/')}/mcp/"
8968
+
8969
+
8970
+ def _toml_string(value: str) -> str:
8971
+ return json_module.dumps(str(value), ensure_ascii=False)
8972
+
8973
+
8974
+ def _print_literal_block(text: str) -> None:
8975
+ """Write copy-paste config without Rich markup or line wrapping."""
8976
+ console.file.write(text)
8977
+ if not text.endswith("\n"):
8978
+ console.file.write("\n")
8979
+ console.file.flush()
8980
+
8981
+
8982
+ def _build_mcp_headers(
8983
+ *,
8984
+ app: str,
8985
+ api_key: str,
8986
+ space_id: str | None = None,
8987
+ ) -> dict[str, str]:
8988
+ headers = {"APP": app}
8989
+ if api_key:
8990
+ headers["Authorization"] = f"Bearer {api_key}"
8991
+ headers["X-NMEM-API-Key"] = api_key
8992
+ if space_id:
8993
+ headers["X-Nmem-Space-Id"] = space_id
8994
+ return headers
8995
+
8996
+
8997
+ def _build_host_mcp_config(
8998
+ host: str,
8999
+ *,
9000
+ app: str | None = None,
9001
+ space_id: str | None = None,
9002
+ ) -> dict[str, Any]:
9003
+ normalized_host = host.strip().lower()
9004
+ if normalized_host not in _MCP_HOST_APPS:
9005
+ raise ValueError(f"Unsupported MCP host: {host}")
9006
+
9007
+ api_url, api_url_source = _resolve_api_url_and_source()
9008
+ api_key = get_api_key()
9009
+ api_key_source = "env" if os.environ.get("NMEM_API_KEY") is not None else (
9010
+ "config" if api_key else "none"
9011
+ )
9012
+ endpoint = _mcp_endpoint_url(api_url)
9013
+ app_header = (app or _MCP_HOST_APPS[normalized_host]).strip()
9014
+ headers = _build_mcp_headers(app=app_header, api_key=api_key, space_id=space_id)
9015
+
9016
+ if normalized_host == "codex":
9017
+ lines = [
9018
+ "[mcp_servers.nowledge-mem]",
9019
+ f"url = {_toml_string(endpoint)}",
9020
+ "",
9021
+ "[mcp_servers.nowledge-mem.http_headers]",
9022
+ ]
9023
+ for key, value in headers.items():
9024
+ toml_key = key if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key) else _toml_string(key)
9025
+ lines.append(f"{toml_key} = {_toml_string(value)}")
9026
+ rendered = "\n".join(lines)
9027
+ config: Any = rendered
9028
+ fmt = "toml"
9029
+ else:
9030
+ server: dict[str, Any]
9031
+ if normalized_host == "gemini-cli":
9032
+ server = {
9033
+ "httpUrl": endpoint,
9034
+ "headers": headers,
9035
+ }
9036
+ else:
9037
+ server = {
9038
+ "url": endpoint,
9039
+ "type": "streamableHttp",
9040
+ "headers": headers,
9041
+ }
9042
+ config = {"mcpServers": {"nowledge-mem": server}}
9043
+ rendered = json_module.dumps(config, indent=2, ensure_ascii=False)
9044
+ fmt = "json"
9045
+
9046
+ warnings: list[str] = []
9047
+ if api_url.startswith("https://") and not api_key:
9048
+ warnings.append("Remote HTTPS endpoint has no API key in client config or NMEM_API_KEY.")
9049
+
9050
+ return {
9051
+ "host": normalized_host,
9052
+ "format": fmt,
9053
+ "config": config,
9054
+ "rendered": rendered,
9055
+ "apiUrl": api_url,
9056
+ "endpoint": endpoint,
9057
+ "apiUrlSource": api_url_source,
9058
+ "apiKeyConfigured": bool(api_key),
9059
+ "apiKeySource": api_key_source,
9060
+ "space": space_id,
9061
+ "warnings": warnings,
9062
+ }
9063
+
9064
+
9065
+ def cmd_config_mcp_show(
9066
+ host: str,
9067
+ *,
9068
+ app: str | None = None,
9069
+ space_id: str | None = None,
9070
+ ) -> None:
9071
+ """Print host-specific MCP config from shared client settings."""
9072
+ try:
9073
+ payload = _build_host_mcp_config(host, app=app, space_id=space_id)
9074
+ except ValueError as exc:
9075
+ if is_json_mode():
9076
+ output_json({"error": "unsupported_host", "message": str(exc)})
9077
+ else:
9078
+ print_error("Unsupported Host", str(exc))
9079
+ return
9080
+
9081
+ if is_json_mode():
9082
+ output_json(payload)
9083
+ return
9084
+
9085
+ console.print()
9086
+ console.print(f"[bold]Nowledge Mem MCP config for {payload['host']}[/bold]")
9087
+ console.print(f" Endpoint: {payload['endpoint']}")
9088
+ console.print(f" URL source: {payload['apiUrlSource']}")
9089
+ console.print(f" API key source: {payload['apiKeySource']}")
9090
+ if payload["apiKeyConfigured"]:
9091
+ console.print(" API key: [yellow]included in the snippet below[/yellow]")
9092
+ for warning in payload["warnings"]:
9093
+ console.print(f" [yellow]Warning:[/yellow] {warning}")
9094
+ console.print()
9095
+ _print_literal_block(payload["rendered"])
9096
+ console.print()
9097
+ console.print(
9098
+ "[dim]This command reads ~/.nowledge-mem/config.json plus NMEM_API_URL/NMEM_API_KEY. "
9099
+ "Direct MCP clients do not read that file by themselves; paste this into the host's MCP config when overriding a bundled local endpoint or connecting to remote Mem.[/dim]"
9100
+ )
9101
+
9102
+
8722
9103
  # ═══════════════════════════════════════════════════════════════════════════════
8723
9104
  # Argument Parser
8724
9105
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -9642,6 +10023,44 @@ PRIORITY
9642
10023
  help="Louvain resolution (higher = more communities)"
9643
10024
  )
9644
10025
 
10026
+ # wiki (with alias 'w')
10027
+ for name in ["wiki", "w"]:
10028
+ wiki_parser = subparsers.add_parser(
10029
+ name, help="LLM Wiki — read pages, export, list topics"
10030
+ )
10031
+ wiki_subs = wiki_parser.add_subparsers(dest="action")
10032
+
10033
+ page = wiki_subs.add_parser(
10034
+ "page", help="Render one wiki page as markdown"
10035
+ )
10036
+ page.add_argument(
10037
+ "kind", choices=["entity", "crystal", "topic"],
10038
+ help="What kind of wiki page to render",
10039
+ )
10040
+ page.add_argument(
10041
+ "id_or_name",
10042
+ help=(
10043
+ "Resource identifier: UUID or canonical name for entity, id "
10044
+ "for crystal, integer community_id for topic"
10045
+ ),
10046
+ )
10047
+
10048
+ export = wiki_subs.add_parser(
10049
+ "export", help="Build the wiki markdown ZIP and save it locally"
10050
+ )
10051
+ export.add_argument(
10052
+ "output_dir", nargs="?", default=None,
10053
+ help="Output directory (default: current working directory)",
10054
+ )
10055
+
10056
+ topics = wiki_subs.add_parser(
10057
+ "topics", help="List wiki topic clusters"
10058
+ )
10059
+ topics.add_argument(
10060
+ "-n", "--limit", type=int, default=20,
10061
+ help="Max topics to show",
10062
+ )
10063
+
9645
10064
  # graph (with alias 'g')
9646
10065
  for name in ["graph", "g"]:
9647
10066
  graph_parser = subparsers.add_parser(
@@ -10093,6 +10512,38 @@ examples:
10093
10512
  help="Setting key: url, api-key, or all (default)",
10094
10513
  )
10095
10514
 
10515
+ # config mcp
10516
+ mcp_parser = cfg_subs.add_parser(
10517
+ "mcp",
10518
+ help="Generate MCP config from this machine's client settings",
10519
+ )
10520
+ mcp_subs = mcp_parser.add_subparsers(dest="action")
10521
+ mcp_show = mcp_subs.add_parser(
10522
+ "show",
10523
+ help="Print host-specific MCP config",
10524
+ epilog="""examples:
10525
+ nmem config mcp show --host codex
10526
+ nmem config mcp show --host gemini-cli
10527
+ nmem config mcp show --host cursor --space Research
10528
+ nmem --json config mcp show --host codex""",
10529
+ formatter_class=argparse.RawDescriptionHelpFormatter,
10530
+ )
10531
+ mcp_show.add_argument(
10532
+ "--host",
10533
+ required=True,
10534
+ choices=sorted(_MCP_HOST_APPS),
10535
+ help="MCP client config shape to print",
10536
+ )
10537
+ mcp_show.add_argument(
10538
+ "--app",
10539
+ help="Override the APP header value (defaults to the selected host)",
10540
+ )
10541
+ mcp_show.add_argument(
10542
+ "--space",
10543
+ dest="space_id",
10544
+ help="Optional Mem space to send as X-Nmem-Space-Id",
10545
+ )
10546
+
10096
10547
  # config settings
10097
10548
  set_parser = cfg_subs.add_parser("settings", help="Knowledge processing settings")
10098
10549
  set_subs = set_parser.add_subparsers(dest="action")
@@ -10497,6 +10948,16 @@ def main() -> int:
10497
10948
  else:
10498
10949
  # Default: list
10499
10950
  cmd_communities_list(getattr(args, "limit", 20))
10951
+ elif cmd in ("wiki", "w"):
10952
+ action = getattr(args, "action", None)
10953
+ if action == "page":
10954
+ cmd_wiki_page(args.kind, args.id_or_name)
10955
+ elif action == "export":
10956
+ cmd_wiki_export(getattr(args, "output_dir", None))
10957
+ elif action == "topics":
10958
+ cmd_wiki_topics(getattr(args, "limit", 20))
10959
+ else:
10960
+ parser.parse_args([cmd, "--help"])
10500
10961
  elif cmd in ("graph", "g"):
10501
10962
  action = args.action
10502
10963
  if action == "expand":
@@ -10672,6 +11133,27 @@ def main() -> int:
10672
11133
  cmd_config_client_clear(getattr(args, "key", "all"))
10673
11134
  else:
10674
11135
  cmd_config_client_show()
11136
+ elif section == "mcp":
11137
+ action = getattr(args, "action", None)
11138
+ if action == "show" or action is None:
11139
+ host = getattr(args, "host", None)
11140
+ if not host:
11141
+ if is_json_mode():
11142
+ output_json({"error": "missing_host", "message": "Pass --host codex|gemini-cli|cursor|claude-desktop"})
11143
+ else:
11144
+ print_error("Missing Host", "Pass --host codex, gemini-cli, cursor, or claude-desktop.")
11145
+ return 1
11146
+ cmd_config_mcp_show(
11147
+ host,
11148
+ app=getattr(args, "app", None),
11149
+ space_id=getattr(args, "space_id", None),
11150
+ )
11151
+ else:
11152
+ cmd_config_mcp_show(
11153
+ getattr(args, "host", "codex"),
11154
+ app=getattr(args, "app", None),
11155
+ space_id=getattr(args, "space_id", None),
11156
+ )
10675
11157
  elif section == "access":
10676
11158
  action = getattr(args, "action", None)
10677
11159
  if action == "set":
@@ -10688,10 +11170,12 @@ def main() -> int:
10688
11170
  cmd_config_settings_show()
10689
11171
  else:
10690
11172
  if is_json_mode():
10691
- output_json({"error": "no_section", "sections": ["provider", "access", "settings"]})
11173
+ output_json({"error": "no_section", "sections": ["provider", "client", "mcp", "access", "settings"]})
10692
11174
  else:
10693
11175
  console.print("[bold]Available config sections:[/bold]")
10694
11176
  console.print(" provider — LLM provider configuration")
11177
+ console.print(" client — Client URL / API key settings")
11178
+ console.print(" mcp — Generate host MCP config from client settings")
10695
11179
  console.print(" access — CLI / server LAN access settings")
10696
11180
  console.print(" settings — Knowledge processing settings")
10697
11181
  else:
@@ -148,7 +148,7 @@ def discover_sessions(
148
148
  session_id: Optional[str],
149
149
  ) -> list[SessionCandidate]:
150
150
  if client == "claude-code":
151
- candidates = _discover_claude_sessions(project_path)
151
+ candidates = _discover_claude_sessions(project_path, session_id)
152
152
  elif client == "codex":
153
153
  candidates = _discover_codex_sessions(project_path, session_id)
154
154
  elif client == "gemini-cli":
@@ -185,7 +185,9 @@ def parse_session(
185
185
  )
186
186
 
187
187
 
188
- def _discover_claude_sessions(project_path: str) -> list[SessionCandidate]:
188
+ def _discover_claude_sessions(
189
+ project_path: str, target_session_id: Optional[str]
190
+ ) -> list[SessionCandidate]:
189
191
  claude_base, expected_claude_base = _resolve_claude_project_dir(project_path)
190
192
  if claude_base is None:
191
193
  raise SessionImportError(
@@ -203,6 +205,7 @@ def _discover_claude_sessions(project_path: str) -> list[SessionCandidate]:
203
205
  with open(session_file, "r", encoding="utf-8") as handle:
204
206
  lines = handle.readlines()
205
207
 
208
+ file_session_id: Optional[str] = None
206
209
  user_messages = 0
207
210
  assistant_messages = 0
208
211
  last_message_time: Optional[float] = None
@@ -216,6 +219,10 @@ def _discover_claude_sessions(project_path: str) -> list[SessionCandidate]:
216
219
  if not isinstance(event, dict):
217
220
  continue
218
221
 
222
+ event_session_id = event.get("sessionId")
223
+ if isinstance(event_session_id, str) and event_session_id.strip():
224
+ file_session_id = event_session_id.strip()
225
+
219
226
  event_type = event.get("type")
220
227
  if event_type in ["file-history-snapshot", "summary"]:
221
228
  continue
@@ -245,12 +252,16 @@ def _discover_claude_sessions(project_path: str) -> list[SessionCandidate]:
245
252
  elif event_type == "assistant":
246
253
  assistant_messages += 1
247
254
 
255
+ file_session_id = file_session_id or session_file.stem
256
+ if target_session_id and file_session_id != target_session_id:
257
+ continue
258
+
248
259
  if user_messages > 0 or assistant_messages > 1:
249
260
  sort_key = last_message_time or session_file.stat().st_mtime
250
261
  candidates.append(
251
262
  SessionCandidate(
252
263
  file=session_file,
253
- session_id=session_file.stem,
264
+ session_id=file_session_id,
254
265
  sort_key=sort_key,
255
266
  user_messages=user_messages,
256
267
  assistant_messages=assistant_messages,
@@ -4,13 +4,11 @@ Nowledge Mem TUI - Terminal User Interface for memory management.
4
4
  Launch with: nmem tui
5
5
 
6
6
  For Textual dev mode:
7
- python -m textual run --dev nmem_cli.tui
7
+ python -m textual run --dev nmem_cli.tui.__main__:app
8
8
  """
9
9
 
10
10
  import logging
11
11
 
12
- from .app import NowledgeMemApp
13
-
14
12
  __all__ = ["NowledgeMemApp", "run_tui"]
15
13
 
16
14
  # Disable all logging to stdout/stderr - it corrupts the TUI display
@@ -19,11 +17,20 @@ logging.getLogger("httpx").setLevel(logging.WARNING)
19
17
  logging.getLogger("httpcore").setLevel(logging.WARNING)
20
18
 
21
19
 
20
+ def _load_app_class():
21
+ from .app import NowledgeMemApp
22
+
23
+ return NowledgeMemApp
24
+
25
+
26
+ def __getattr__(name: str):
27
+ if name == "NowledgeMemApp":
28
+ return _load_app_class()
29
+ raise AttributeError(name)
30
+
31
+
22
32
  def run_tui() -> None:
23
33
  """Entry point for the TUI application."""
34
+ NowledgeMemApp = _load_app_class()
24
35
  app = NowledgeMemApp()
25
36
  app.run()
26
-
27
-
28
- # For textual run --dev compatibility
29
- app = NowledgeMemApp()
@@ -2,7 +2,7 @@
2
2
  Entry point for running TUI with Textual dev mode.
3
3
 
4
4
  Usage:
5
- python -m textual run --dev nmem_cli.tui
5
+ python -m textual run --dev nmem_cli.tui.__main__:app
6
6
 
7
7
  Or directly:
8
8
  python -m nmem_cli.tui
File without changes
File without changes