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.
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/PKG-INFO +16 -1
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/README.md +15 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/pyproject.toml +1 -1
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/__init__.py +1 -1
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/cli.py +391 -1
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/session_import.py +14 -3
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/__init__.py +14 -7
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/__main__.py +1 -1
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/.gitignore +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/license_payload.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/py.typed +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/api_client.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/app.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/__init__.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/dashboard.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/graph.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/help.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/memories.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/settings.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
- {nmem_cli-0.7.9 → nmem_cli-0.8.0}/src/nmem_cli/tui/screens/threads.py +0 -0
- {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.
|
|
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 |
|
|
@@ -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(
|
|
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=
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|