nmem-cli 0.7.6__tar.gz → 0.7.9__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/.gitignore +1 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/PKG-INFO +1 -1
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/pyproject.toml +1 -1
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/__init__.py +1 -1
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/cli.py +421 -42
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/session_import.py +64 -24
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/api_client.py +1 -0
- nmem_cli-0.7.6/.gitignore +0 -5
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/README.md +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/license_payload.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/py.typed +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/__init__.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/__main__.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/app.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/__init__.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/dashboard.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/graph.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/help.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/memories.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/settings.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/screens/threads.py +0 -0
- {nmem_cli-0.7.6 → nmem_cli-0.7.9}/src/nmem_cli/tui/widgets/__init__.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
config.env
|
|
@@ -557,6 +557,43 @@ def _is_loopback_hostname(hostname: str | None) -> bool:
|
|
|
557
557
|
return False
|
|
558
558
|
|
|
559
559
|
|
|
560
|
+
def _httpx_transport_kwargs(url: str) -> dict[str, Any]:
|
|
561
|
+
"""Disable environment proxy settings for loopback URLs.
|
|
562
|
+
|
|
563
|
+
Windows proxy configuration can incorrectly capture localhost traffic for
|
|
564
|
+
CLI/TUI requests. Loopback calls should always go directly to the local Mem
|
|
565
|
+
server instead of honoring system proxy settings.
|
|
566
|
+
"""
|
|
567
|
+
try:
|
|
568
|
+
hostname = urlsplit(url).hostname
|
|
569
|
+
except Exception:
|
|
570
|
+
hostname = None
|
|
571
|
+
return {"trust_env": False} if _is_loopback_hostname(hostname) else {}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _merge_httpx_kwargs(url: str, kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
575
|
+
merged = dict(kwargs)
|
|
576
|
+
for key, value in _httpx_transport_kwargs(url).items():
|
|
577
|
+
merged.setdefault(key, value)
|
|
578
|
+
return merged
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _httpx_request(method: str, url: str, **kwargs: Any) -> httpx.Response:
|
|
582
|
+
return httpx.request(method, url, **_merge_httpx_kwargs(url, kwargs))
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _httpx_get(url: str, **kwargs: Any) -> httpx.Response:
|
|
586
|
+
return httpx.get(url, **_merge_httpx_kwargs(url, kwargs))
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _httpx_post(url: str, **kwargs: Any) -> httpx.Response:
|
|
590
|
+
return httpx.post(url, **_merge_httpx_kwargs(url, kwargs))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _httpx_stream(method: str, url: str, **kwargs: Any):
|
|
594
|
+
return httpx.stream(method, url, **_merge_httpx_kwargs(url, kwargs))
|
|
595
|
+
|
|
596
|
+
|
|
560
597
|
def _iter_local_management_base_urls() -> list[str]:
|
|
561
598
|
candidates: list[str] = []
|
|
562
599
|
seen: set[str] = set()
|
|
@@ -585,7 +622,7 @@ def _iter_local_management_base_urls() -> list[str]:
|
|
|
585
622
|
def _request_local_remote_access_management(path: str) -> dict[str, Any] | None:
|
|
586
623
|
for base_url in _iter_local_management_base_urls():
|
|
587
624
|
try:
|
|
588
|
-
response =
|
|
625
|
+
response = _httpx_post(f"{base_url}{path}", timeout=10.0)
|
|
589
626
|
except httpx.RequestError:
|
|
590
627
|
continue
|
|
591
628
|
|
|
@@ -744,7 +781,7 @@ def api_request(
|
|
|
744
781
|
merged_headers.update(headers)
|
|
745
782
|
|
|
746
783
|
cookies = get_api_cookies() or None
|
|
747
|
-
response =
|
|
784
|
+
response = _httpx_request(
|
|
748
785
|
method,
|
|
749
786
|
url,
|
|
750
787
|
params=params,
|
|
@@ -760,7 +797,7 @@ def api_request(
|
|
|
760
797
|
# If auth headers are stripped in transit, retry once with query-key fallback.
|
|
761
798
|
if get_api_key() and _is_missing_api_key_response(response):
|
|
762
799
|
retry_params = _build_params_with_query_key(params)
|
|
763
|
-
response =
|
|
800
|
+
response = _httpx_request(
|
|
764
801
|
method,
|
|
765
802
|
url,
|
|
766
803
|
params=retry_params,
|
|
@@ -779,7 +816,7 @@ def api_request(
|
|
|
779
816
|
compat_url = _strip_remote_api_path_prefix(url)
|
|
780
817
|
if compat_url != url:
|
|
781
818
|
retry_params = _build_params_with_query_key(params)
|
|
782
|
-
response =
|
|
819
|
+
response = _httpx_request(
|
|
783
820
|
method,
|
|
784
821
|
compat_url,
|
|
785
822
|
params=retry_params,
|
|
@@ -1636,7 +1673,7 @@ def _plugin_detection_probes() -> (
|
|
|
1636
1673
|
def _fetch_plugin_registry() -> list[dict]:
|
|
1637
1674
|
"""Fetch the community integrations registry. Uses cache on failure. Never raises."""
|
|
1638
1675
|
try:
|
|
1639
|
-
resp =
|
|
1676
|
+
resp = _httpx_get(_REGISTRY_URL, timeout=10.0, headers={"User-Agent": f"nmem-cli/{__version__}"})
|
|
1640
1677
|
resp.raise_for_status()
|
|
1641
1678
|
data = resp.json()
|
|
1642
1679
|
# Cache for offline use
|
|
@@ -4523,8 +4560,10 @@ def cmd_threads_save(
|
|
|
4523
4560
|
)
|
|
4524
4561
|
sys.exit(1)
|
|
4525
4562
|
|
|
4526
|
-
#
|
|
4527
|
-
|
|
4563
|
+
# Use an absolute lexical path instead of resolving symlinks. Session stores
|
|
4564
|
+
# record the user's working directory string, not the filesystem canonical
|
|
4565
|
+
# realpath, so eager resolve() can drift from Claude/Codex/Gemini metadata.
|
|
4566
|
+
resolved_path = Path(project_path).expanduser().absolute()
|
|
4528
4567
|
if not resolved_path.exists():
|
|
4529
4568
|
if is_json_mode():
|
|
4530
4569
|
output_json({"error": "path_not_found", "path": str(resolved_path)})
|
|
@@ -5171,7 +5210,7 @@ def cmd_sources_list(
|
|
|
5171
5210
|
return
|
|
5172
5211
|
|
|
5173
5212
|
if not sources:
|
|
5174
|
-
console.print("[dim]No
|
|
5213
|
+
console.print("[dim]No artifacts in Library[/dim]")
|
|
5175
5214
|
return
|
|
5176
5215
|
|
|
5177
5216
|
table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
|
|
@@ -5219,7 +5258,7 @@ def cmd_sources_show(source_id: str, space_id: str | None = None) -> None:
|
|
|
5219
5258
|
return
|
|
5220
5259
|
|
|
5221
5260
|
console.print(f"\n[bold]{data.get('original_name', source_id)}[/bold]")
|
|
5222
|
-
console.print(f" [dim]ID[/dim]
|
|
5261
|
+
console.print(f" [dim]Artifact ID[/dim] {data.get('id', '')}")
|
|
5223
5262
|
console.print(f" [dim]Type[/dim] {data.get('source_type', '')}")
|
|
5224
5263
|
console.print(f" [dim]MIME[/dim] {data.get('mime_type', '')}")
|
|
5225
5264
|
console.print(f" [dim]State[/dim] {data.get('lifecycle_state', '')}")
|
|
@@ -5275,9 +5314,9 @@ def cmd_sources_delete(
|
|
|
5275
5314
|
|
|
5276
5315
|
if not normalized_ids:
|
|
5277
5316
|
if is_json_mode():
|
|
5278
|
-
output_json({"error": "invalid_input", "message": "No
|
|
5317
|
+
output_json({"error": "invalid_input", "message": "No artifact IDs provided"})
|
|
5279
5318
|
else:
|
|
5280
|
-
print_error("Invalid Input", "No
|
|
5319
|
+
print_error("Invalid Input", "No artifact IDs provided")
|
|
5281
5320
|
return
|
|
5282
5321
|
|
|
5283
5322
|
if dry_run:
|
|
@@ -5316,14 +5355,14 @@ def cmd_sources_delete(
|
|
|
5316
5355
|
else:
|
|
5317
5356
|
console.print(f" [yellow]- not found[/yellow] [dim]{p['id']}[/dim]")
|
|
5318
5357
|
n = len(normalized_ids)
|
|
5319
|
-
console.print(f"\n [dim]{found} of {n} {'
|
|
5358
|
+
console.print(f"\n [dim]{found} of {n} {'artifact' if n == 1 else 'artifacts'} found, would be deleted.[/dim]")
|
|
5320
5359
|
return
|
|
5321
5360
|
|
|
5322
5361
|
if not force and not is_json_mode():
|
|
5323
5362
|
if len(normalized_ids) == 1:
|
|
5324
|
-
prompt = f"Delete
|
|
5363
|
+
prompt = f"Delete artifact {normalized_ids[0]}? This removes all files, chunks, and graph links."
|
|
5325
5364
|
else:
|
|
5326
|
-
prompt = f"Delete {len(normalized_ids)}
|
|
5365
|
+
prompt = f"Delete {len(normalized_ids)} artifacts? This removes all files, chunks, and graph links."
|
|
5327
5366
|
if not Confirm.ask(prompt):
|
|
5328
5367
|
console.print("[dim]Cancelled[/dim]")
|
|
5329
5368
|
return
|
|
@@ -5364,7 +5403,7 @@ def cmd_sources_delete(
|
|
|
5364
5403
|
# api_delete already prints error on failure
|
|
5365
5404
|
else:
|
|
5366
5405
|
if failed == 0:
|
|
5367
|
-
print_success("Deleted", f"{deleted}
|
|
5406
|
+
print_success("Deleted", f"{deleted} artifacts")
|
|
5368
5407
|
else:
|
|
5369
5408
|
print_error(
|
|
5370
5409
|
"Bulk delete completed with failures",
|
|
@@ -5372,6 +5411,250 @@ def cmd_sources_delete(
|
|
|
5372
5411
|
)
|
|
5373
5412
|
|
|
5374
5413
|
|
|
5414
|
+
def cmd_sources_search(
|
|
5415
|
+
query: str,
|
|
5416
|
+
limit: int = 20,
|
|
5417
|
+
space_id: str | None = None,
|
|
5418
|
+
) -> None:
|
|
5419
|
+
"""Search across Library artifact names and summaries (FTS)."""
|
|
5420
|
+
params: dict[str, Any] = {"q": query, "limit": limit}
|
|
5421
|
+
if space_id:
|
|
5422
|
+
params["space_id"] = space_id
|
|
5423
|
+
|
|
5424
|
+
if not is_json_mode():
|
|
5425
|
+
with Progress(
|
|
5426
|
+
SpinnerColumn(),
|
|
5427
|
+
TextColumn(f"[cyan]Searching Library for '{query}'...[/cyan]"),
|
|
5428
|
+
console=console,
|
|
5429
|
+
transient=True,
|
|
5430
|
+
) as p:
|
|
5431
|
+
p.add_task("", total=None)
|
|
5432
|
+
data = api_get("/sources/search", params=params, timeout=30.0)
|
|
5433
|
+
else:
|
|
5434
|
+
data = api_get("/sources/search", params=params, timeout=30.0)
|
|
5435
|
+
|
|
5436
|
+
results = data.get("results", [])
|
|
5437
|
+
|
|
5438
|
+
if is_json_mode():
|
|
5439
|
+
output_json(data)
|
|
5440
|
+
return
|
|
5441
|
+
|
|
5442
|
+
if not results:
|
|
5443
|
+
console.print(f"[dim]No Library artifacts matched '{query}'[/dim]")
|
|
5444
|
+
return
|
|
5445
|
+
|
|
5446
|
+
console.print(
|
|
5447
|
+
f"[green]Found {len(results)} matches[/green] for [cyan]{query}[/cyan]\n"
|
|
5448
|
+
)
|
|
5449
|
+
table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
|
|
5450
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
5451
|
+
table.add_column("Title")
|
|
5452
|
+
table.add_column("Type", style="dim")
|
|
5453
|
+
table.add_column("Score", justify="right", width=6)
|
|
5454
|
+
table.add_column("Snippet", style="dim")
|
|
5455
|
+
for r in results:
|
|
5456
|
+
table.add_row(
|
|
5457
|
+
r.get("source_id", ""),
|
|
5458
|
+
truncate(r.get("title", ""), 40),
|
|
5459
|
+
r.get("source_type", ""),
|
|
5460
|
+
format_score(r.get("score", 0.0)),
|
|
5461
|
+
truncate(r.get("snippet", ""), 60),
|
|
5462
|
+
)
|
|
5463
|
+
console.print(table)
|
|
5464
|
+
|
|
5465
|
+
|
|
5466
|
+
def cmd_sources_read(
|
|
5467
|
+
source_id: str,
|
|
5468
|
+
offset: int = 0,
|
|
5469
|
+
limit: int = 8000,
|
|
5470
|
+
space_id: str | None = None,
|
|
5471
|
+
) -> None:
|
|
5472
|
+
"""Read the parsed content of a Library artifact."""
|
|
5473
|
+
params: dict[str, Any] = {
|
|
5474
|
+
"offset": offset,
|
|
5475
|
+
"limit": limit,
|
|
5476
|
+
}
|
|
5477
|
+
if space_id:
|
|
5478
|
+
params["space_id"] = space_id
|
|
5479
|
+
|
|
5480
|
+
if not is_json_mode():
|
|
5481
|
+
with Progress(
|
|
5482
|
+
SpinnerColumn(),
|
|
5483
|
+
TextColumn("[cyan]Loading artifact content...[/cyan]"),
|
|
5484
|
+
console=console,
|
|
5485
|
+
transient=True,
|
|
5486
|
+
) as p:
|
|
5487
|
+
p.add_task("", total=None)
|
|
5488
|
+
data = api_get(f"/sources/{source_id}/content", params=params or None, timeout=30.0)
|
|
5489
|
+
else:
|
|
5490
|
+
data = api_get(f"/sources/{source_id}/content", params=params or None, timeout=30.0)
|
|
5491
|
+
|
|
5492
|
+
window = data.get("content", "") or ""
|
|
5493
|
+
total_length_value = data.get("total_length")
|
|
5494
|
+
returned_length_value = data.get("returned_length")
|
|
5495
|
+
window_offset_value = data.get("offset")
|
|
5496
|
+
total_length = int(total_length_value) if total_length_value is not None else len(window)
|
|
5497
|
+
returned_length = (
|
|
5498
|
+
int(returned_length_value) if returned_length_value is not None else len(window)
|
|
5499
|
+
)
|
|
5500
|
+
window_offset = int(window_offset_value) if window_offset_value is not None else offset
|
|
5501
|
+
window_end = window_offset + returned_length
|
|
5502
|
+
has_more = bool(data.get("has_more"))
|
|
5503
|
+
|
|
5504
|
+
if is_json_mode():
|
|
5505
|
+
output_json({
|
|
5506
|
+
"source_id": source_id,
|
|
5507
|
+
"original_name": data.get("original_name", ""),
|
|
5508
|
+
"offset": window_offset,
|
|
5509
|
+
"returned_length": returned_length,
|
|
5510
|
+
"total_length": total_length,
|
|
5511
|
+
"has_more": has_more,
|
|
5512
|
+
"content": window,
|
|
5513
|
+
})
|
|
5514
|
+
return
|
|
5515
|
+
|
|
5516
|
+
console.print(f"\n[bold]{data.get('original_name', source_id)}[/bold]")
|
|
5517
|
+
console.print(f" [dim]Artifact ID[/dim] {source_id}")
|
|
5518
|
+
console.print(
|
|
5519
|
+
f" [dim]Bytes[/dim] {window_offset}..{window_end} of {total_length}"
|
|
5520
|
+
)
|
|
5521
|
+
console.print()
|
|
5522
|
+
console.print(window)
|
|
5523
|
+
if has_more:
|
|
5524
|
+
console.print(
|
|
5525
|
+
f"\n[dim]... {total_length - window_end} more characters. Use --offset {window_end} to continue.[/dim]"
|
|
5526
|
+
)
|
|
5527
|
+
|
|
5528
|
+
|
|
5529
|
+
def cmd_sources_search_chunks(
|
|
5530
|
+
source_id: str,
|
|
5531
|
+
query: str,
|
|
5532
|
+
limit: int = 5,
|
|
5533
|
+
space_id: str | None = None,
|
|
5534
|
+
) -> None:
|
|
5535
|
+
"""Search within a single artifact's indexed chunks (BM25)."""
|
|
5536
|
+
arguments: dict[str, Any] = {
|
|
5537
|
+
"source_id": source_id,
|
|
5538
|
+
"query": query,
|
|
5539
|
+
"limit": limit,
|
|
5540
|
+
}
|
|
5541
|
+
if space_id:
|
|
5542
|
+
arguments["space_id"] = space_id
|
|
5543
|
+
|
|
5544
|
+
if not is_json_mode():
|
|
5545
|
+
with Progress(
|
|
5546
|
+
SpinnerColumn(),
|
|
5547
|
+
TextColumn(f"[cyan]Searching artifact sections for '{query}'...[/cyan]"),
|
|
5548
|
+
console=console,
|
|
5549
|
+
transient=True,
|
|
5550
|
+
) as p:
|
|
5551
|
+
p.add_task("", total=None)
|
|
5552
|
+
data = api_mcp_tool_call("search_source_chunks", arguments)
|
|
5553
|
+
else:
|
|
5554
|
+
data = api_mcp_tool_call("search_source_chunks", arguments)
|
|
5555
|
+
|
|
5556
|
+
if is_json_mode():
|
|
5557
|
+
output_json(data)
|
|
5558
|
+
return
|
|
5559
|
+
|
|
5560
|
+
if data.get("status") == "error":
|
|
5561
|
+
print_error("Search failed", data.get("error", "Unknown error"))
|
|
5562
|
+
return
|
|
5563
|
+
|
|
5564
|
+
chunks = data.get("chunks", [])
|
|
5565
|
+
source_name = data.get("source_name") or source_id
|
|
5566
|
+
if not chunks:
|
|
5567
|
+
console.print(f"[dim]No chunks matched '{query}' in {source_name}[/dim]")
|
|
5568
|
+
return
|
|
5569
|
+
|
|
5570
|
+
console.print(
|
|
5571
|
+
f"[green]Matched {len(chunks)}[/green] of {data.get('total_chunks', '?')} chunks "
|
|
5572
|
+
f"in [bold]{source_name}[/bold]\n"
|
|
5573
|
+
)
|
|
5574
|
+
for i, chunk in enumerate(chunks, 1):
|
|
5575
|
+
idx = chunk.get("chunk_index")
|
|
5576
|
+
score = chunk.get("score")
|
|
5577
|
+
score_str = f" [dim]score={format_score(score)}[/dim]" if score is not None else ""
|
|
5578
|
+
idx_str = f"chunk {idx}" if idx is not None else f"match {i}"
|
|
5579
|
+
console.print(f"[bold cyan]{idx_str}[/bold cyan]{score_str}")
|
|
5580
|
+
text = chunk.get("text", "")
|
|
5581
|
+
preview = text if len(text) < 800 else text[:800] + "..."
|
|
5582
|
+
console.print(preview)
|
|
5583
|
+
console.print()
|
|
5584
|
+
|
|
5585
|
+
|
|
5586
|
+
def cmd_sources_analyze(
|
|
5587
|
+
source_id: str,
|
|
5588
|
+
columns: list[str] | None = None,
|
|
5589
|
+
space_id: str | None = None,
|
|
5590
|
+
) -> None:
|
|
5591
|
+
"""Analyze a tabular Library artifact (CSV/TSV/XLSX/XLS)."""
|
|
5592
|
+
arguments: dict[str, Any] = {"source_id": source_id}
|
|
5593
|
+
if columns:
|
|
5594
|
+
arguments["columns"] = columns
|
|
5595
|
+
if space_id:
|
|
5596
|
+
arguments["space_id"] = space_id
|
|
5597
|
+
|
|
5598
|
+
if not is_json_mode():
|
|
5599
|
+
with Progress(
|
|
5600
|
+
SpinnerColumn(),
|
|
5601
|
+
TextColumn("[cyan]Analyzing artifact...[/cyan]"),
|
|
5602
|
+
console=console,
|
|
5603
|
+
transient=True,
|
|
5604
|
+
) as p:
|
|
5605
|
+
p.add_task("", total=None)
|
|
5606
|
+
data = api_mcp_tool_call("analyze_source_data", arguments)
|
|
5607
|
+
else:
|
|
5608
|
+
data = api_mcp_tool_call("analyze_source_data", arguments)
|
|
5609
|
+
|
|
5610
|
+
if is_json_mode():
|
|
5611
|
+
output_json(data)
|
|
5612
|
+
return
|
|
5613
|
+
|
|
5614
|
+
if data.get("status") == "error":
|
|
5615
|
+
print_error("Analysis failed", data.get("error", "Unknown error"))
|
|
5616
|
+
return
|
|
5617
|
+
|
|
5618
|
+
source_name = data.get("source_name") or source_id
|
|
5619
|
+
console.print(f"\n[bold]{source_name}[/bold]")
|
|
5620
|
+
sheets = data.get("sheets") or ([data] if data.get("columns") else [])
|
|
5621
|
+
if not sheets:
|
|
5622
|
+
console.print("[dim]Analyzer returned no sheet data[/dim]")
|
|
5623
|
+
console.print()
|
|
5624
|
+
console.print_json(data=data)
|
|
5625
|
+
return
|
|
5626
|
+
|
|
5627
|
+
for sheet in sheets:
|
|
5628
|
+
sheet_name = sheet.get("sheet_name") or sheet.get("name")
|
|
5629
|
+
if sheet_name:
|
|
5630
|
+
console.print(f"\n[bold]Sheet:[/bold] {sheet_name}")
|
|
5631
|
+
rows = sheet.get("row_count") or sheet.get("rows")
|
|
5632
|
+
cols_meta = sheet.get("columns") or []
|
|
5633
|
+
if rows is not None:
|
|
5634
|
+
console.print(f" [dim]Rows[/dim] {rows}")
|
|
5635
|
+
if cols_meta:
|
|
5636
|
+
console.print(f" [dim]Columns[/dim] {len(cols_meta)}")
|
|
5637
|
+
table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
|
|
5638
|
+
table.add_column("Column", style="cyan")
|
|
5639
|
+
table.add_column("Type", style="dim")
|
|
5640
|
+
table.add_column("Non-null", justify="right")
|
|
5641
|
+
table.add_column("Summary", style="dim")
|
|
5642
|
+
for col in cols_meta:
|
|
5643
|
+
name = col.get("name", "")
|
|
5644
|
+
dtype = col.get("dtype", col.get("type", ""))
|
|
5645
|
+
non_null = col.get("non_null_count", col.get("count", ""))
|
|
5646
|
+
summary_bits: list[str] = []
|
|
5647
|
+
if col.get("min") is not None and col.get("max") is not None:
|
|
5648
|
+
summary_bits.append(f"min={col['min']} max={col['max']}")
|
|
5649
|
+
if col.get("mean") is not None:
|
|
5650
|
+
summary_bits.append(f"mean={col['mean']:.2f}")
|
|
5651
|
+
if col.get("top"):
|
|
5652
|
+
summary_bits.append(f"top={col['top']}")
|
|
5653
|
+
table.add_row(name, str(dtype), str(non_null), ", ".join(summary_bits))
|
|
5654
|
+
console.print(table)
|
|
5655
|
+
console.print()
|
|
5656
|
+
|
|
5657
|
+
|
|
5375
5658
|
def cmd_sources_add(
|
|
5376
5659
|
targets: list[str],
|
|
5377
5660
|
comment: str | None = None,
|
|
@@ -5572,7 +5855,7 @@ def _get_remote_access_status() -> dict[str, Any] | None:
|
|
|
5572
5855
|
"""Fetch Access Anywhere status from the local backend (GET)."""
|
|
5573
5856
|
for base_url in _iter_local_management_base_urls():
|
|
5574
5857
|
try:
|
|
5575
|
-
response =
|
|
5858
|
+
response = _httpx_get(
|
|
5576
5859
|
f"{base_url}/api/remote-access/status", timeout=10.0
|
|
5577
5860
|
)
|
|
5578
5861
|
except httpx.RequestError:
|
|
@@ -6411,7 +6694,7 @@ def _check_for_update(platform_id: str) -> Optional[dict]:
|
|
|
6411
6694
|
"""Call backbone API to check for updates. Returns parsed JSON or None."""
|
|
6412
6695
|
url = f"{_UPDATE_CHECK_URL}?platform={platform_id}&secure=false"
|
|
6413
6696
|
try:
|
|
6414
|
-
resp =
|
|
6697
|
+
resp = _httpx_get(
|
|
6415
6698
|
url,
|
|
6416
6699
|
headers={"Accept": "application/json", "User-Agent": _UPDATE_USER_AGENT},
|
|
6417
6700
|
timeout=30.0,
|
|
@@ -6922,7 +7205,7 @@ def _update_appimage(yes: bool) -> None:
|
|
|
6922
7205
|
) as p:
|
|
6923
7206
|
p.add_task("", total=None)
|
|
6924
7207
|
try:
|
|
6925
|
-
resp =
|
|
7208
|
+
resp = _httpx_get(
|
|
6926
7209
|
f"{_UPDATE_CHECK_URL}?platform=linux-appimage&secure=true",
|
|
6927
7210
|
headers={"Accept": "application/json", "User-Agent": _UPDATE_USER_AGENT},
|
|
6928
7211
|
timeout=30.0,
|
|
@@ -7008,7 +7291,7 @@ def _update_appimage(yes: bool) -> None:
|
|
|
7008
7291
|
suffix=".AppImage", dir=str(appimage_path.parent), delete=False,
|
|
7009
7292
|
) as tmp:
|
|
7010
7293
|
temp_path = Path(tmp.name)
|
|
7011
|
-
with
|
|
7294
|
+
with _httpx_stream(
|
|
7012
7295
|
"GET", download_url, timeout=600.0, follow_redirects=True,
|
|
7013
7296
|
) as stream:
|
|
7014
7297
|
stream.raise_for_status()
|
|
@@ -8480,9 +8763,9 @@ EXAMPLES
|
|
|
8480
8763
|
nmem wm patch --heading "## Notes" --append "extra note" Append to a section
|
|
8481
8764
|
nmem wm history List past WM dates
|
|
8482
8765
|
nmem --json m search "x" JSON output
|
|
8483
|
-
nmem
|
|
8484
|
-
nmem
|
|
8485
|
-
nmem
|
|
8766
|
+
nmem library List Library artifacts
|
|
8767
|
+
nmem library show <id> Show artifact details
|
|
8768
|
+
nmem library delete <id> Delete an artifact
|
|
8486
8769
|
nmem g expand <id> Graph connections for a memory
|
|
8487
8770
|
nmem f Activity feed (last 7 days)
|
|
8488
8771
|
nmem f --days 1 Today's activity
|
|
@@ -8493,7 +8776,8 @@ ALIASES
|
|
|
8493
8776
|
m = memories
|
|
8494
8777
|
t = threads
|
|
8495
8778
|
wm = working-memory
|
|
8496
|
-
|
|
8779
|
+
l = library
|
|
8780
|
+
s = sources (legacy alias)
|
|
8497
8781
|
g = graph
|
|
8498
8782
|
f = feed
|
|
8499
8783
|
c = communities
|
|
@@ -9506,25 +9790,31 @@ PRIORITY
|
|
|
9506
9790
|
spaces_subs.add_parser("enable", help="Turn on memory spaces")
|
|
9507
9791
|
spaces_subs.add_parser("disable", help="Turn off memory spaces (only when no extra spaces remain)")
|
|
9508
9792
|
|
|
9509
|
-
#
|
|
9510
|
-
for name in [
|
|
9793
|
+
# library artifacts (legacy aliases: sources, s)
|
|
9794
|
+
for name, help_text in [
|
|
9795
|
+
("library", "Library — files, web pages, notes, and other artifacts"),
|
|
9796
|
+
("lib", argparse.SUPPRESS),
|
|
9797
|
+
("l", argparse.SUPPRESS),
|
|
9798
|
+
("sources", argparse.SUPPRESS),
|
|
9799
|
+
("s", argparse.SUPPRESS),
|
|
9800
|
+
]:
|
|
9511
9801
|
src_parser = subparsers.add_parser(
|
|
9512
|
-
name, help=
|
|
9802
|
+
name, help=help_text, parents=[_ambient_space_parent]
|
|
9513
9803
|
)
|
|
9514
9804
|
src_subs = src_parser.add_subparsers(dest="action")
|
|
9515
9805
|
|
|
9516
9806
|
ls = src_subs.add_parser(
|
|
9517
9807
|
"list",
|
|
9518
|
-
help="List
|
|
9808
|
+
help="List Library artifacts (default)",
|
|
9519
9809
|
parents=[_space_parent],
|
|
9520
9810
|
epilog="""examples:
|
|
9521
|
-
nmem
|
|
9522
|
-
nmem
|
|
9523
|
-
nmem
|
|
9811
|
+
nmem library
|
|
9812
|
+
nmem library list --type url -n 50
|
|
9813
|
+
nmem library list --state error""",
|
|
9524
9814
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
9525
9815
|
)
|
|
9526
9816
|
ls.add_argument(
|
|
9527
|
-
"-n", "--limit", type=int, default=20, help="Max
|
|
9817
|
+
"-n", "--limit", type=int, default=20, help="Max artifacts to show"
|
|
9528
9818
|
)
|
|
9529
9819
|
ls.add_argument(
|
|
9530
9820
|
"--type", dest="source_type",
|
|
@@ -9548,7 +9838,7 @@ PRIORITY
|
|
|
9548
9838
|
add.add_argument("target", nargs="+", help="File path(s) or URL(s) to ingest")
|
|
9549
9839
|
add.add_argument(
|
|
9550
9840
|
"-c", "--comment", dest="user_comment",
|
|
9551
|
-
help="Comment about the
|
|
9841
|
+
help="Comment about the artifact",
|
|
9552
9842
|
)
|
|
9553
9843
|
add.add_argument(
|
|
9554
9844
|
"-l", "--label", dest="labels", action="append",
|
|
@@ -9557,26 +9847,26 @@ PRIORITY
|
|
|
9557
9847
|
|
|
9558
9848
|
sh = src_subs.add_parser(
|
|
9559
9849
|
"show",
|
|
9560
|
-
help="Show
|
|
9850
|
+
help="Show artifact details",
|
|
9561
9851
|
parents=[_space_parent],
|
|
9562
9852
|
epilog="""examples:
|
|
9563
|
-
nmem
|
|
9564
|
-
nmem --json
|
|
9853
|
+
nmem library show src-abc123
|
|
9854
|
+
nmem --json library show src-abc123""",
|
|
9565
9855
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
9566
9856
|
)
|
|
9567
|
-
sh.add_argument("id", help="
|
|
9857
|
+
sh.add_argument("id", help="Artifact ID")
|
|
9568
9858
|
|
|
9569
9859
|
d = src_subs.add_parser(
|
|
9570
9860
|
"delete",
|
|
9571
|
-
help="Delete
|
|
9861
|
+
help="Delete artifact(s)",
|
|
9572
9862
|
parents=[_space_parent],
|
|
9573
9863
|
epilog="""examples:
|
|
9574
|
-
nmem
|
|
9575
|
-
nmem
|
|
9576
|
-
nmem
|
|
9864
|
+
nmem library delete src-abc123
|
|
9865
|
+
nmem library delete src-abc123 src-def456 --force
|
|
9866
|
+
nmem library delete src-abc123 --dry-run""",
|
|
9577
9867
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
9578
9868
|
)
|
|
9579
|
-
d.add_argument("id", nargs="+", help="
|
|
9869
|
+
d.add_argument("id", nargs="+", help="Artifact ID(s) to delete")
|
|
9580
9870
|
d.add_argument(
|
|
9581
9871
|
"-f", "--force", action="store_true",
|
|
9582
9872
|
help="Skip confirmation prompt",
|
|
@@ -9586,6 +9876,69 @@ PRIORITY
|
|
|
9586
9876
|
help="Preview what would be deleted without making changes",
|
|
9587
9877
|
)
|
|
9588
9878
|
|
|
9879
|
+
srch = src_subs.add_parser(
|
|
9880
|
+
"search",
|
|
9881
|
+
help="Search Library artifacts by name/summary (FTS)",
|
|
9882
|
+
parents=[_space_parent],
|
|
9883
|
+
epilog="""examples:
|
|
9884
|
+
nmem library search "meeting notes"
|
|
9885
|
+
nmem library search roadmap -n 10""",
|
|
9886
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
9887
|
+
)
|
|
9888
|
+
srch.add_argument("query", help="Search query")
|
|
9889
|
+
srch.add_argument(
|
|
9890
|
+
"-n", "--limit", type=int, default=20, help="Max results (default 20)"
|
|
9891
|
+
)
|
|
9892
|
+
|
|
9893
|
+
rd = src_subs.add_parser(
|
|
9894
|
+
"read",
|
|
9895
|
+
help="Read parsed artifact content",
|
|
9896
|
+
parents=[_space_parent],
|
|
9897
|
+
epilog="""examples:
|
|
9898
|
+
nmem library read src-abc123
|
|
9899
|
+
nmem library read src-abc123 --offset 8000 --limit 8000""",
|
|
9900
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
9901
|
+
)
|
|
9902
|
+
rd.add_argument("id", help="Artifact ID")
|
|
9903
|
+
rd.add_argument(
|
|
9904
|
+
"--offset", type=int, default=0, help="Character offset (default 0)"
|
|
9905
|
+
)
|
|
9906
|
+
rd.add_argument(
|
|
9907
|
+
"--limit", type=int, default=8000,
|
|
9908
|
+
help="Max characters to return (default 8000)",
|
|
9909
|
+
)
|
|
9910
|
+
|
|
9911
|
+
sc = src_subs.add_parser(
|
|
9912
|
+
"search-chunks",
|
|
9913
|
+
help="Search within an artifact's indexed chunks",
|
|
9914
|
+
parents=[_space_parent],
|
|
9915
|
+
epilog="""examples:
|
|
9916
|
+
nmem library search-chunks src-abc123 "risk factors"
|
|
9917
|
+
nmem library search-chunks src-abc123 budget -n 3""",
|
|
9918
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
9919
|
+
)
|
|
9920
|
+
sc.add_argument("id", help="Artifact ID")
|
|
9921
|
+
sc.add_argument("query", help="Search query")
|
|
9922
|
+
sc.add_argument(
|
|
9923
|
+
"-n", "--limit", type=int, default=5,
|
|
9924
|
+
help="Max matching chunks (1-20, default 5)",
|
|
9925
|
+
)
|
|
9926
|
+
|
|
9927
|
+
an = src_subs.add_parser(
|
|
9928
|
+
"analyze",
|
|
9929
|
+
help="Analyze tabular artifact (CSV/TSV/XLSX/XLS)",
|
|
9930
|
+
parents=[_space_parent],
|
|
9931
|
+
epilog="""examples:
|
|
9932
|
+
nmem library analyze src-abc123
|
|
9933
|
+
nmem library analyze src-abc123 --column price --column region""",
|
|
9934
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
9935
|
+
)
|
|
9936
|
+
an.add_argument("id", help="Artifact ID")
|
|
9937
|
+
an.add_argument(
|
|
9938
|
+
"--column", dest="columns", action="append",
|
|
9939
|
+
help="Column name to analyze (repeatable)",
|
|
9940
|
+
)
|
|
9941
|
+
|
|
9589
9942
|
# license
|
|
9590
9943
|
lic_parser = subparsers.add_parser("license", help="License management")
|
|
9591
9944
|
lic_subs = lic_parser.add_subparsers(dest="action")
|
|
@@ -10213,7 +10566,7 @@ def main() -> int:
|
|
|
10213
10566
|
cmd_spaces_set_enabled(False)
|
|
10214
10567
|
else:
|
|
10215
10568
|
cmd_spaces_list()
|
|
10216
|
-
elif cmd in ("sources", "s"):
|
|
10569
|
+
elif cmd in ("library", "lib", "l", "sources", "s"):
|
|
10217
10570
|
action = args.action
|
|
10218
10571
|
if action == "add":
|
|
10219
10572
|
cmd_sources_add(
|
|
@@ -10226,6 +10579,32 @@ def main() -> int:
|
|
|
10226
10579
|
cmd_sources_show(args.id, getattr(args, "space", None))
|
|
10227
10580
|
elif action == "delete":
|
|
10228
10581
|
cmd_sources_delete(args.id, getattr(args, "force", False), getattr(args, "dry_run", False), getattr(args, "space", None))
|
|
10582
|
+
elif action == "search":
|
|
10583
|
+
cmd_sources_search(
|
|
10584
|
+
query=args.query,
|
|
10585
|
+
limit=getattr(args, "limit", 20),
|
|
10586
|
+
space_id=getattr(args, "space", None),
|
|
10587
|
+
)
|
|
10588
|
+
elif action == "read":
|
|
10589
|
+
cmd_sources_read(
|
|
10590
|
+
source_id=args.id,
|
|
10591
|
+
offset=getattr(args, "offset", 0),
|
|
10592
|
+
limit=getattr(args, "limit", 8000),
|
|
10593
|
+
space_id=getattr(args, "space", None),
|
|
10594
|
+
)
|
|
10595
|
+
elif action == "search-chunks":
|
|
10596
|
+
cmd_sources_search_chunks(
|
|
10597
|
+
source_id=args.id,
|
|
10598
|
+
query=args.query,
|
|
10599
|
+
limit=getattr(args, "limit", 5),
|
|
10600
|
+
space_id=getattr(args, "space", None),
|
|
10601
|
+
)
|
|
10602
|
+
elif action == "analyze":
|
|
10603
|
+
cmd_sources_analyze(
|
|
10604
|
+
source_id=args.id,
|
|
10605
|
+
columns=getattr(args, "columns", None),
|
|
10606
|
+
space_id=getattr(args, "space", None),
|
|
10607
|
+
)
|
|
10229
10608
|
else:
|
|
10230
10609
|
# Default: list
|
|
10231
10610
|
cmd_sources_list(
|
|
@@ -29,6 +29,30 @@ from typing import Any, Optional
|
|
|
29
29
|
logger = logging.getLogger(__name__)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def _project_path_variants(project_path: str) -> list[str]:
|
|
33
|
+
variants: list[str] = []
|
|
34
|
+
|
|
35
|
+
def _add(candidate: str | None) -> None:
|
|
36
|
+
if not candidate:
|
|
37
|
+
return
|
|
38
|
+
normalized = os.path.normpath(candidate)
|
|
39
|
+
if normalized not in variants:
|
|
40
|
+
variants.append(normalized)
|
|
41
|
+
|
|
42
|
+
expanded = os.path.expanduser(project_path)
|
|
43
|
+
_add(expanded)
|
|
44
|
+
_add(os.path.abspath(expanded))
|
|
45
|
+
_add(os.path.realpath(expanded))
|
|
46
|
+
|
|
47
|
+
for existing in list(variants):
|
|
48
|
+
if existing.startswith("/private/"):
|
|
49
|
+
_add(existing[len("/private"):])
|
|
50
|
+
elif existing.startswith(("/var/", "/tmp/", "/etc/")):
|
|
51
|
+
_add("/private" + existing)
|
|
52
|
+
|
|
53
|
+
return variants
|
|
54
|
+
|
|
55
|
+
|
|
32
56
|
def _env_path(env_var: str, default: Path) -> Path:
|
|
33
57
|
raw = os.environ.get(env_var, "").strip()
|
|
34
58
|
if not raw:
|
|
@@ -47,33 +71,45 @@ def _get_codex_sessions_dir() -> Path:
|
|
|
47
71
|
def _encode_claude_project_path(project_path: str) -> str:
|
|
48
72
|
return _encode_claude_project_path_variant(
|
|
49
73
|
project_path,
|
|
50
|
-
|
|
74
|
+
dot_mode="all_as_hyphen",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _encode_claude_project_path_hidden_dot_compat(project_path: str) -> str:
|
|
79
|
+
return _encode_claude_project_path_variant(
|
|
80
|
+
project_path,
|
|
81
|
+
dot_mode="hidden_only",
|
|
51
82
|
)
|
|
52
83
|
|
|
53
84
|
|
|
54
85
|
def _encode_claude_project_path_legacy(project_path: str) -> str:
|
|
55
86
|
return _encode_claude_project_path_variant(
|
|
56
87
|
project_path,
|
|
57
|
-
|
|
88
|
+
dot_mode="preserve",
|
|
58
89
|
)
|
|
59
90
|
|
|
60
91
|
|
|
61
92
|
def _resolve_claude_project_dir(project_path: str) -> tuple[Optional[Path], Path]:
|
|
62
93
|
claude_projects_dir = _get_claude_projects_dir()
|
|
94
|
+
project_path_variants = _project_path_variants(project_path)
|
|
63
95
|
candidate_dirs: list[Path] = []
|
|
64
|
-
for
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
96
|
+
for path_variant in project_path_variants:
|
|
97
|
+
for encoded in (
|
|
98
|
+
_encode_claude_project_path(path_variant),
|
|
99
|
+
_encode_claude_project_path_hidden_dot_compat(path_variant),
|
|
100
|
+
_encode_claude_project_path_legacy(path_variant),
|
|
101
|
+
):
|
|
102
|
+
candidate = claude_projects_dir / encoded
|
|
103
|
+
if candidate not in candidate_dirs:
|
|
104
|
+
candidate_dirs.append(candidate)
|
|
71
105
|
|
|
72
106
|
for candidate in candidate_dirs:
|
|
73
107
|
if candidate.exists() and candidate.is_dir():
|
|
74
108
|
return candidate, candidate_dirs[0]
|
|
75
109
|
|
|
76
|
-
|
|
110
|
+
normalized_project_paths = {
|
|
111
|
+
_normalize_project_path(path_variant) for path_variant in project_path_variants
|
|
112
|
+
}
|
|
77
113
|
if claude_projects_dir.exists() and claude_projects_dir.is_dir():
|
|
78
114
|
for project_dir in claude_projects_dir.iterdir():
|
|
79
115
|
if not project_dir.is_dir():
|
|
@@ -82,7 +118,7 @@ def _resolve_claude_project_dir(project_path: str) -> tuple[Optional[Path], Path
|
|
|
82
118
|
decoded_path = _decode_claude_project_dir_name(project_dir.name)
|
|
83
119
|
if (
|
|
84
120
|
decoded_path is not None
|
|
85
|
-
and _normalize_project_path(decoded_path)
|
|
121
|
+
and _normalize_project_path(decoded_path) in normalized_project_paths
|
|
86
122
|
):
|
|
87
123
|
return project_dir, candidate_dirs[0]
|
|
88
124
|
|
|
@@ -1391,10 +1427,11 @@ def _find_valid_path(base: str, remaining: list[str]) -> Optional[str]:
|
|
|
1391
1427
|
return result
|
|
1392
1428
|
|
|
1393
1429
|
if len(remaining) >= 2:
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1430
|
+
for delimiter in ("-", "."):
|
|
1431
|
+
combined = remaining[0] + delimiter + remaining[1]
|
|
1432
|
+
result = _find_valid_path(base, [combined] + remaining[2:])
|
|
1433
|
+
if result:
|
|
1434
|
+
return result
|
|
1398
1435
|
|
|
1399
1436
|
return None
|
|
1400
1437
|
|
|
@@ -1441,16 +1478,17 @@ def _find_valid_path_windows(base: str, remaining: list[str]) -> Optional[str]:
|
|
|
1441
1478
|
return result
|
|
1442
1479
|
|
|
1443
1480
|
if len(remaining) >= 2:
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1481
|
+
for delimiter in ("-", "."):
|
|
1482
|
+
combined = remaining[0] + delimiter + remaining[1]
|
|
1483
|
+
result = _find_valid_path_windows(base, [combined] + remaining[2:])
|
|
1484
|
+
if result:
|
|
1485
|
+
return result
|
|
1448
1486
|
|
|
1449
1487
|
return None
|
|
1450
1488
|
|
|
1451
1489
|
|
|
1452
1490
|
def _encode_claude_project_path_variant(
|
|
1453
|
-
project_path: str, *,
|
|
1491
|
+
project_path: str, *, dot_mode: str
|
|
1454
1492
|
) -> str:
|
|
1455
1493
|
windows_match = re.match(r"^([A-Za-z]):[\\/](.*)$", project_path)
|
|
1456
1494
|
if windows_match:
|
|
@@ -1459,7 +1497,7 @@ def _encode_claude_project_path_variant(
|
|
|
1459
1497
|
segments = [
|
|
1460
1498
|
_encode_claude_path_segment(
|
|
1461
1499
|
segment,
|
|
1462
|
-
|
|
1500
|
+
dot_mode=dot_mode,
|
|
1463
1501
|
)
|
|
1464
1502
|
for segment in re.split(r"[\\/]+", rest)
|
|
1465
1503
|
if segment
|
|
@@ -1467,7 +1505,7 @@ def _encode_claude_project_path_variant(
|
|
|
1467
1505
|
return "-".join([drive_letter, *segments]) if segments else drive_letter
|
|
1468
1506
|
|
|
1469
1507
|
segments = [
|
|
1470
|
-
_encode_claude_path_segment(segment,
|
|
1508
|
+
_encode_claude_path_segment(segment, dot_mode=dot_mode)
|
|
1471
1509
|
for segment in project_path.replace("\\", "/").split("/")
|
|
1472
1510
|
if segment
|
|
1473
1511
|
]
|
|
@@ -1475,9 +1513,11 @@ def _encode_claude_project_path_variant(
|
|
|
1475
1513
|
|
|
1476
1514
|
|
|
1477
1515
|
def _encode_claude_path_segment(
|
|
1478
|
-
segment: str, *,
|
|
1516
|
+
segment: str, *, dot_mode: str
|
|
1479
1517
|
) -> str:
|
|
1480
|
-
if
|
|
1518
|
+
if dot_mode == "all_as_hyphen":
|
|
1519
|
+
return segment.replace(".", "-")
|
|
1520
|
+
if dot_mode == "hidden_only" and segment.startswith(".") and len(segment) > 1:
|
|
1481
1521
|
return "-" + segment[1:]
|
|
1482
1522
|
return segment
|
|
1483
1523
|
|
nmem_cli-0.7.6/.gitignore
DELETED
|
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
|
|
File without changes
|