nmem-cli 0.7.9__tar.gz → 0.8.0__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.0}/PKG-INFO +16 -1
  2. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/README.md +15 -0
  3. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/pyproject.toml +1 -1
  4. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/__init__.py +1 -1
  5. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/cli.py +391 -1
  6. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/session_import.py +14 -3
  7. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/__init__.py +14 -7
  8. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/__main__.py +1 -1
  9. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/.gitignore +0 -0
  10. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/license_payload.py +0 -0
  11. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/py.typed +0 -0
  12. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/api_client.py +0 -0
  13. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/app.py +0 -0
  14. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/__init__.py +0 -0
  15. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/dashboard.py +0 -0
  16. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/graph.py +0 -0
  17. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/help.py +0 -0
  18. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/memories.py +0 -0
  19. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
  20. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/settings.py +0 -0
  21. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
  22. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/threads.py +0 -0
  23. {nmem_cli-0.7.9 → nmem_cli-0.8.0}/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.0
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.0"
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.0"
24
24
  __author__ = "Nowledge Labs"
25
25
 
26
26
  from .cli import main
@@ -2082,6 +2082,147 @@ def cmd_communities_detect(resolution: float = 1.0) -> None:
2082
2082
  console.print(f" [dim]{data['message']}[/dim]")
2083
2083
 
2084
2084
 
2085
+ # ═══════════════════════════════════════════════════════════════════════════════
2086
+ # Wiki Commands
2087
+ # ═══════════════════════════════════════════════════════════════════════════════
2088
+
2089
+
2090
+ def cmd_wiki_page(kind: str, id_or_name: str) -> None:
2091
+ """Render one Library wiki page as markdown."""
2092
+ kind_norm = (kind or "").strip().lower()
2093
+ if kind_norm not in {"entity", "crystal", "topic"}:
2094
+ print_error(
2095
+ "Bad kind",
2096
+ f"kind must be one of 'entity', 'crystal', 'topic'; got {kind!r}",
2097
+ )
2098
+ sys.exit(2)
2099
+ encoded = quote(id_or_name, safe="")
2100
+ url = f"{get_api_url()}/library/wiki-page/{kind_norm}/{encoded}"
2101
+ try:
2102
+ response = api_request("GET", url, timeout=30.0)
2103
+ except httpx.ConnectError:
2104
+ if is_json_mode():
2105
+ output_json({
2106
+ "error": "connection_failed",
2107
+ "message": f"Cannot connect to {get_api_url()}",
2108
+ })
2109
+ else:
2110
+ _print_connection_error()
2111
+ sys.exit(1)
2112
+ if response.status_code == 404:
2113
+ if is_json_mode():
2114
+ output_json({
2115
+ "error": "not_found", "kind": kind_norm, "id_or_name": id_or_name,
2116
+ })
2117
+ else:
2118
+ console.print(f"[red]Not found:[/red] {kind_norm} {id_or_name}")
2119
+ sys.exit(1)
2120
+ response.raise_for_status()
2121
+ text = response.text
2122
+ if is_json_mode():
2123
+ output_json({
2124
+ "kind": kind_norm,
2125
+ "id_or_name": id_or_name,
2126
+ "markdown": text,
2127
+ })
2128
+ else:
2129
+ console.print(Markdown(text))
2130
+
2131
+
2132
+ def cmd_wiki_export(output_dir: Optional[str] = None) -> None:
2133
+ """Build the wiki ZIP and save it locally."""
2134
+ url = f"{get_api_url()}/library/wiki-export"
2135
+ try:
2136
+ if not is_json_mode():
2137
+ with Progress(
2138
+ SpinnerColumn(),
2139
+ TextColumn("[cyan]Building wiki ZIP...[/cyan]"),
2140
+ console=console,
2141
+ transient=True,
2142
+ ) as p:
2143
+ p.add_task("", total=None)
2144
+ response = api_request("GET", url, timeout=300.0)
2145
+ else:
2146
+ response = api_request("GET", url, timeout=300.0)
2147
+ except httpx.ConnectError:
2148
+ if is_json_mode():
2149
+ output_json({
2150
+ "error": "connection_failed",
2151
+ "message": f"Cannot connect to {get_api_url()}",
2152
+ })
2153
+ else:
2154
+ _print_connection_error()
2155
+ sys.exit(1)
2156
+ response.raise_for_status()
2157
+ cd = response.headers.get("content-disposition", "")
2158
+ match = re.search(r'filename="([^"]+)"', cd)
2159
+ filename = match.group(1) if match else "nowledge-mem-wiki.zip"
2160
+ target_dir = Path(output_dir).expanduser() if output_dir else Path.cwd()
2161
+ target_dir.mkdir(parents=True, exist_ok=True)
2162
+ target = target_dir / filename
2163
+ target.write_bytes(response.content)
2164
+ if is_json_mode():
2165
+ output_json({
2166
+ "saved_to": str(target),
2167
+ "size_bytes": len(response.content),
2168
+ })
2169
+ else:
2170
+ console.print(
2171
+ f"[green]Saved[/green] {len(response.content):,} bytes to [bold]{target}[/bold]"
2172
+ )
2173
+ console.print(
2174
+ "[dim]Open the folder in Obsidian, Logseq, or any markdown reader; "
2175
+ "wikilinks resolve in all of them.[/dim]"
2176
+ )
2177
+
2178
+
2179
+ def cmd_wiki_topics(limit: int = 20) -> None:
2180
+ """List wiki topics (community clusters) with their counts."""
2181
+ if not is_json_mode():
2182
+ with Progress(
2183
+ SpinnerColumn(),
2184
+ TextColumn("[cyan]Loading topics...[/cyan]"),
2185
+ console=console,
2186
+ transient=True,
2187
+ ) as p:
2188
+ p.add_task("", total=None)
2189
+ data = api_get(
2190
+ "/library/wiki-index",
2191
+ params={"community_limit": limit, "top_per_community": 5},
2192
+ )
2193
+ else:
2194
+ data = api_get(
2195
+ "/library/wiki-index",
2196
+ params={"community_limit": limit, "top_per_community": 5},
2197
+ )
2198
+ if is_json_mode():
2199
+ output_json(data)
2200
+ return
2201
+ communities = data.get("communities", [])
2202
+ console.print()
2203
+ if not communities:
2204
+ console.print(
2205
+ "[dim]No topics yet. Run community detection from the Graph view "
2206
+ "(or `nmem c detect`) to populate them.[/dim]"
2207
+ )
2208
+ console.print()
2209
+ return
2210
+ console.print(f"[bold]Wiki topics[/bold] [dim]({len(communities)} found)[/dim]")
2211
+ console.print()
2212
+ for c in communities:
2213
+ name = c.get("name", "Unnamed")
2214
+ members = c.get("member_count", 0)
2215
+ ent = len(c.get("top_entities", []))
2216
+ cry = len(c.get("top_crystals", []))
2217
+ console.print(
2218
+ f" [bold cyan]{name}[/bold cyan] "
2219
+ f"[dim]({members} members · {cry} crystals · {ent} entities)[/dim]"
2220
+ )
2221
+ cid = c.get("community_id", "?")
2222
+ console.print(f" [dim]nmem wiki page topic {cid}[/dim]")
2223
+ console.print()
2224
+
2225
+
2085
2226
  # ═══════════════════════════════════════════════════════════════════════════════
2086
2227
  # Graph Commands
2087
2228
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -4643,6 +4784,7 @@ def cmd_threads_save(
4643
4784
  {
4644
4785
  "messages": thread_data["messages"],
4645
4786
  "deduplicate": True,
4787
+ **({"space_id": space_id} if space_id else {}),
4646
4788
  },
4647
4789
  )
4648
4790
  results.append(
@@ -8719,6 +8861,151 @@ def cmd_config_client_clear(key: str | None = None) -> None:
8719
8861
  )
8720
8862
 
8721
8863
 
8864
+ _MCP_HOST_APPS = {
8865
+ "codex": "Codex",
8866
+ "gemini-cli": "Gemini CLI",
8867
+ "cursor": "Cursor",
8868
+ "claude-desktop": "Claude",
8869
+ }
8870
+
8871
+
8872
+ def _mcp_endpoint_url(api_url: str) -> str:
8873
+ return f"{api_url.rstrip('/')}/mcp/"
8874
+
8875
+
8876
+ def _toml_string(value: str) -> str:
8877
+ return json_module.dumps(str(value), ensure_ascii=False)
8878
+
8879
+
8880
+ def _print_literal_block(text: str) -> None:
8881
+ """Write copy-paste config without Rich markup or line wrapping."""
8882
+ console.file.write(text)
8883
+ if not text.endswith("\n"):
8884
+ console.file.write("\n")
8885
+ console.file.flush()
8886
+
8887
+
8888
+ def _build_mcp_headers(
8889
+ *,
8890
+ app: str,
8891
+ api_key: str,
8892
+ space_id: str | None = None,
8893
+ ) -> dict[str, str]:
8894
+ headers = {"APP": app}
8895
+ if api_key:
8896
+ headers["Authorization"] = f"Bearer {api_key}"
8897
+ headers["X-NMEM-API-Key"] = api_key
8898
+ if space_id:
8899
+ headers["X-Nmem-Space-Id"] = space_id
8900
+ return headers
8901
+
8902
+
8903
+ def _build_host_mcp_config(
8904
+ host: str,
8905
+ *,
8906
+ app: str | None = None,
8907
+ space_id: str | None = None,
8908
+ ) -> dict[str, Any]:
8909
+ normalized_host = host.strip().lower()
8910
+ if normalized_host not in _MCP_HOST_APPS:
8911
+ raise ValueError(f"Unsupported MCP host: {host}")
8912
+
8913
+ api_url, api_url_source = _resolve_api_url_and_source()
8914
+ api_key = get_api_key()
8915
+ api_key_source = "env" if os.environ.get("NMEM_API_KEY") is not None else (
8916
+ "config" if api_key else "none"
8917
+ )
8918
+ endpoint = _mcp_endpoint_url(api_url)
8919
+ app_header = (app or _MCP_HOST_APPS[normalized_host]).strip()
8920
+ headers = _build_mcp_headers(app=app_header, api_key=api_key, space_id=space_id)
8921
+
8922
+ if normalized_host == "codex":
8923
+ lines = [
8924
+ "[mcp_servers.nowledge-mem]",
8925
+ f"url = {_toml_string(endpoint)}",
8926
+ "",
8927
+ "[mcp_servers.nowledge-mem.http_headers]",
8928
+ ]
8929
+ for key, value in headers.items():
8930
+ toml_key = key if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key) else _toml_string(key)
8931
+ lines.append(f"{toml_key} = {_toml_string(value)}")
8932
+ rendered = "\n".join(lines)
8933
+ config: Any = rendered
8934
+ fmt = "toml"
8935
+ else:
8936
+ server: dict[str, Any]
8937
+ if normalized_host == "gemini-cli":
8938
+ server = {
8939
+ "httpUrl": endpoint,
8940
+ "headers": headers,
8941
+ }
8942
+ else:
8943
+ server = {
8944
+ "url": endpoint,
8945
+ "type": "streamableHttp",
8946
+ "headers": headers,
8947
+ }
8948
+ config = {"mcpServers": {"nowledge-mem": server}}
8949
+ rendered = json_module.dumps(config, indent=2, ensure_ascii=False)
8950
+ fmt = "json"
8951
+
8952
+ warnings: list[str] = []
8953
+ if api_url.startswith("https://") and not api_key:
8954
+ warnings.append("Remote HTTPS endpoint has no API key in client config or NMEM_API_KEY.")
8955
+
8956
+ return {
8957
+ "host": normalized_host,
8958
+ "format": fmt,
8959
+ "config": config,
8960
+ "rendered": rendered,
8961
+ "apiUrl": api_url,
8962
+ "endpoint": endpoint,
8963
+ "apiUrlSource": api_url_source,
8964
+ "apiKeyConfigured": bool(api_key),
8965
+ "apiKeySource": api_key_source,
8966
+ "space": space_id,
8967
+ "warnings": warnings,
8968
+ }
8969
+
8970
+
8971
+ def cmd_config_mcp_show(
8972
+ host: str,
8973
+ *,
8974
+ app: str | None = None,
8975
+ space_id: str | None = None,
8976
+ ) -> None:
8977
+ """Print host-specific MCP config from shared client settings."""
8978
+ try:
8979
+ payload = _build_host_mcp_config(host, app=app, space_id=space_id)
8980
+ except ValueError as exc:
8981
+ if is_json_mode():
8982
+ output_json({"error": "unsupported_host", "message": str(exc)})
8983
+ else:
8984
+ print_error("Unsupported Host", str(exc))
8985
+ return
8986
+
8987
+ if is_json_mode():
8988
+ output_json(payload)
8989
+ return
8990
+
8991
+ console.print()
8992
+ console.print(f"[bold]Nowledge Mem MCP config for {payload['host']}[/bold]")
8993
+ console.print(f" Endpoint: {payload['endpoint']}")
8994
+ console.print(f" URL source: {payload['apiUrlSource']}")
8995
+ console.print(f" API key source: {payload['apiKeySource']}")
8996
+ if payload["apiKeyConfigured"]:
8997
+ console.print(" API key: [yellow]included in the snippet below[/yellow]")
8998
+ for warning in payload["warnings"]:
8999
+ console.print(f" [yellow]Warning:[/yellow] {warning}")
9000
+ console.print()
9001
+ _print_literal_block(payload["rendered"])
9002
+ console.print()
9003
+ console.print(
9004
+ "[dim]This command reads ~/.nowledge-mem/config.json plus NMEM_API_URL/NMEM_API_KEY. "
9005
+ "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]"
9006
+ )
9007
+
9008
+
8722
9009
  # ═══════════════════════════════════════════════════════════════════════════════
8723
9010
  # Argument Parser
8724
9011
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -9642,6 +9929,44 @@ PRIORITY
9642
9929
  help="Louvain resolution (higher = more communities)"
9643
9930
  )
9644
9931
 
9932
+ # wiki (with alias 'w')
9933
+ for name in ["wiki", "w"]:
9934
+ wiki_parser = subparsers.add_parser(
9935
+ name, help="LLM Wiki — read pages, export, list topics"
9936
+ )
9937
+ wiki_subs = wiki_parser.add_subparsers(dest="action")
9938
+
9939
+ page = wiki_subs.add_parser(
9940
+ "page", help="Render one wiki page as markdown"
9941
+ )
9942
+ page.add_argument(
9943
+ "kind", choices=["entity", "crystal", "topic"],
9944
+ help="What kind of wiki page to render",
9945
+ )
9946
+ page.add_argument(
9947
+ "id_or_name",
9948
+ help=(
9949
+ "Resource identifier: UUID or canonical name for entity, id "
9950
+ "for crystal, integer community_id for topic"
9951
+ ),
9952
+ )
9953
+
9954
+ export = wiki_subs.add_parser(
9955
+ "export", help="Build the wiki markdown ZIP and save it locally"
9956
+ )
9957
+ export.add_argument(
9958
+ "output_dir", nargs="?", default=None,
9959
+ help="Output directory (default: current working directory)",
9960
+ )
9961
+
9962
+ topics = wiki_subs.add_parser(
9963
+ "topics", help="List wiki topic clusters"
9964
+ )
9965
+ topics.add_argument(
9966
+ "-n", "--limit", type=int, default=20,
9967
+ help="Max topics to show",
9968
+ )
9969
+
9645
9970
  # graph (with alias 'g')
9646
9971
  for name in ["graph", "g"]:
9647
9972
  graph_parser = subparsers.add_parser(
@@ -10093,6 +10418,38 @@ examples:
10093
10418
  help="Setting key: url, api-key, or all (default)",
10094
10419
  )
10095
10420
 
10421
+ # config mcp
10422
+ mcp_parser = cfg_subs.add_parser(
10423
+ "mcp",
10424
+ help="Generate MCP config from this machine's client settings",
10425
+ )
10426
+ mcp_subs = mcp_parser.add_subparsers(dest="action")
10427
+ mcp_show = mcp_subs.add_parser(
10428
+ "show",
10429
+ help="Print host-specific MCP config",
10430
+ epilog="""examples:
10431
+ nmem config mcp show --host codex
10432
+ nmem config mcp show --host gemini-cli
10433
+ nmem config mcp show --host cursor --space Research
10434
+ nmem --json config mcp show --host codex""",
10435
+ formatter_class=argparse.RawDescriptionHelpFormatter,
10436
+ )
10437
+ mcp_show.add_argument(
10438
+ "--host",
10439
+ required=True,
10440
+ choices=sorted(_MCP_HOST_APPS),
10441
+ help="MCP client config shape to print",
10442
+ )
10443
+ mcp_show.add_argument(
10444
+ "--app",
10445
+ help="Override the APP header value (defaults to the selected host)",
10446
+ )
10447
+ mcp_show.add_argument(
10448
+ "--space",
10449
+ dest="space_id",
10450
+ help="Optional Mem space to send as X-Nmem-Space-Id",
10451
+ )
10452
+
10096
10453
  # config settings
10097
10454
  set_parser = cfg_subs.add_parser("settings", help="Knowledge processing settings")
10098
10455
  set_subs = set_parser.add_subparsers(dest="action")
@@ -10497,6 +10854,16 @@ def main() -> int:
10497
10854
  else:
10498
10855
  # Default: list
10499
10856
  cmd_communities_list(getattr(args, "limit", 20))
10857
+ elif cmd in ("wiki", "w"):
10858
+ action = getattr(args, "action", None)
10859
+ if action == "page":
10860
+ cmd_wiki_page(args.kind, args.id_or_name)
10861
+ elif action == "export":
10862
+ cmd_wiki_export(getattr(args, "output_dir", None))
10863
+ elif action == "topics":
10864
+ cmd_wiki_topics(getattr(args, "limit", 20))
10865
+ else:
10866
+ parser.parse_args([cmd, "--help"])
10500
10867
  elif cmd in ("graph", "g"):
10501
10868
  action = args.action
10502
10869
  if action == "expand":
@@ -10672,6 +11039,27 @@ def main() -> int:
10672
11039
  cmd_config_client_clear(getattr(args, "key", "all"))
10673
11040
  else:
10674
11041
  cmd_config_client_show()
11042
+ elif section == "mcp":
11043
+ action = getattr(args, "action", None)
11044
+ if action == "show" or action is None:
11045
+ host = getattr(args, "host", None)
11046
+ if not host:
11047
+ if is_json_mode():
11048
+ output_json({"error": "missing_host", "message": "Pass --host codex|gemini-cli|cursor|claude-desktop"})
11049
+ else:
11050
+ print_error("Missing Host", "Pass --host codex, gemini-cli, cursor, or claude-desktop.")
11051
+ return 1
11052
+ cmd_config_mcp_show(
11053
+ host,
11054
+ app=getattr(args, "app", None),
11055
+ space_id=getattr(args, "space_id", None),
11056
+ )
11057
+ else:
11058
+ cmd_config_mcp_show(
11059
+ getattr(args, "host", "codex"),
11060
+ app=getattr(args, "app", None),
11061
+ space_id=getattr(args, "space_id", None),
11062
+ )
10675
11063
  elif section == "access":
10676
11064
  action = getattr(args, "action", None)
10677
11065
  if action == "set":
@@ -10688,10 +11076,12 @@ def main() -> int:
10688
11076
  cmd_config_settings_show()
10689
11077
  else:
10690
11078
  if is_json_mode():
10691
- output_json({"error": "no_section", "sections": ["provider", "access", "settings"]})
11079
+ output_json({"error": "no_section", "sections": ["provider", "client", "mcp", "access", "settings"]})
10692
11080
  else:
10693
11081
  console.print("[bold]Available config sections:[/bold]")
10694
11082
  console.print(" provider — LLM provider configuration")
11083
+ console.print(" client — Client URL / API key settings")
11084
+ console.print(" mcp — Generate host MCP config from client settings")
10695
11085
  console.print(" access — CLI / server LAN access settings")
10696
11086
  console.print(" settings — Knowledge processing settings")
10697
11087
  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