nmem-cli 0.10.4__tar.gz → 0.10.6__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 (36) hide show
  1. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/Cargo.lock +13 -13
  2. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/Cargo.toml +1 -1
  3. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/PKG-INFO +1 -1
  4. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/cli.rs +19 -3
  5. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/main.rs +180 -0
  6. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/pyproject.toml +1 -1
  7. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/README.md +0 -0
  8. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/Cargo.toml +0 -0
  9. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/README.md +0 -0
  10. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/client.rs +0 -0
  11. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/agents.rs +0 -0
  12. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/communities.rs +0 -0
  13. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/data_transfer.rs +0 -0
  14. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/feed.rs +0 -0
  15. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/fs.rs +0 -0
  16. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/graph.rs +0 -0
  17. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/library.rs +0 -0
  18. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/license.rs +0 -0
  19. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/memories.rs +0 -0
  20. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/memory_relations.rs +0 -0
  21. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/mod.rs +0 -0
  22. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/models.rs +0 -0
  23. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/plugins.rs +0 -0
  24. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/provider.rs +0 -0
  25. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/rules.rs +0 -0
  26. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/session_import.rs +0 -0
  27. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/skills.rs +0 -0
  28. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/spaces.rs +0 -0
  29. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/stubs.rs +0 -0
  30. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/system.rs +0 -0
  31. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/threads.rs +0 -0
  32. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/wiki.rs +0 -0
  33. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/commands/working_memory.rs +0 -0
  34. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/config.rs +0 -0
  35. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/format.rs +0 -0
  36. {nmem_cli-0.10.4 → nmem_cli-0.10.6}/crates/nmem-cli/src/style.rs +0 -0
@@ -5206,7 +5206,7 @@ dependencies = [
5206
5206
 
5207
5207
  [[package]]
5208
5208
  name = "nmem-cli"
5209
- version = "0.10.4"
5209
+ version = "0.10.6"
5210
5210
  dependencies = [
5211
5211
  "anstyle",
5212
5212
  "anyhow",
@@ -5222,7 +5222,7 @@ dependencies = [
5222
5222
 
5223
5223
  [[package]]
5224
5224
  name = "nmem-content"
5225
- version = "0.10.4"
5225
+ version = "0.10.6"
5226
5226
  dependencies = [
5227
5227
  "anyhow",
5228
5228
  "md5",
@@ -5234,7 +5234,7 @@ dependencies = [
5234
5234
 
5235
5235
  [[package]]
5236
5236
  name = "nmem-core"
5237
- version = "0.10.4"
5237
+ version = "0.10.6"
5238
5238
  dependencies = [
5239
5239
  "serde",
5240
5240
  "serde_json",
@@ -5244,7 +5244,7 @@ dependencies = [
5244
5244
 
5245
5245
  [[package]]
5246
5246
  name = "nmem-docs"
5247
- version = "0.10.4"
5247
+ version = "0.10.6"
5248
5248
  dependencies = [
5249
5249
  "criterion",
5250
5250
  "scraper",
@@ -5254,7 +5254,7 @@ dependencies = [
5254
5254
 
5255
5255
  [[package]]
5256
5256
  name = "nmem-embed"
5257
- version = "0.10.4"
5257
+ version = "0.10.6"
5258
5258
  dependencies = [
5259
5259
  "anyhow",
5260
5260
  "arrow-array",
@@ -5274,7 +5274,7 @@ dependencies = [
5274
5274
 
5275
5275
  [[package]]
5276
5276
  name = "nmem-feed"
5277
- version = "0.10.4"
5277
+ version = "0.10.6"
5278
5278
  dependencies = [
5279
5279
  "anyhow",
5280
5280
  "serde_json",
@@ -5283,7 +5283,7 @@ dependencies = [
5283
5283
 
5284
5284
  [[package]]
5285
5285
  name = "nmem-graph"
5286
- version = "0.10.4"
5286
+ version = "0.10.6"
5287
5287
  dependencies = [
5288
5288
  "anyhow",
5289
5289
  "caseless",
@@ -5302,7 +5302,7 @@ dependencies = [
5302
5302
 
5303
5303
  [[package]]
5304
5304
  name = "nmem-harness"
5305
- version = "0.10.4"
5305
+ version = "0.10.6"
5306
5306
  dependencies = [
5307
5307
  "anyhow",
5308
5308
  "futures",
@@ -5322,7 +5322,7 @@ dependencies = [
5322
5322
 
5323
5323
  [[package]]
5324
5324
  name = "nmem-model"
5325
- version = "0.10.4"
5325
+ version = "0.10.6"
5326
5326
  dependencies = [
5327
5327
  "anyhow",
5328
5328
  "encoding_rs",
@@ -5351,7 +5351,7 @@ dependencies = [
5351
5351
 
5352
5352
  [[package]]
5353
5353
  name = "nmem-search"
5354
- version = "0.10.4"
5354
+ version = "0.10.6"
5355
5355
  dependencies = [
5356
5356
  "anyhow",
5357
5357
  "arrow-array",
@@ -5373,7 +5373,7 @@ dependencies = [
5373
5373
 
5374
5374
  [[package]]
5375
5375
  name = "nmem-server"
5376
- version = "0.10.4"
5376
+ version = "0.10.6"
5377
5377
  dependencies = [
5378
5378
  "aes-gcm",
5379
5379
  "anyhow",
@@ -5429,7 +5429,7 @@ dependencies = [
5429
5429
 
5430
5430
  [[package]]
5431
5431
  name = "nmem-tui"
5432
- version = "0.10.4"
5432
+ version = "0.10.6"
5433
5433
  dependencies = [
5434
5434
  "anyhow",
5435
5435
  "arboard",
@@ -5443,7 +5443,7 @@ dependencies = [
5443
5443
 
5444
5444
  [[package]]
5445
5445
  name = "nmem-websearch"
5446
- version = "0.10.4"
5446
+ version = "0.10.6"
5447
5447
  dependencies = [
5448
5448
  "anyhow",
5449
5449
  "dom_smoothie",
@@ -4,7 +4,7 @@ members = ["crates/nmem-cli"]
4
4
 
5
5
  [workspace.package]
6
6
  edition = "2021"
7
- version = "0.10.4"
7
+ version = "0.10.6"
8
8
  license = "UNLICENSED"
9
9
  publish = false
10
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nmem-cli
3
- Version: 0.10.4
3
+ Version: 0.10.6
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -178,6 +178,19 @@ pub enum Command {
178
178
  #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
179
179
  passthrough: Vec<String>,
180
180
  },
181
+ /// Run the backend server (headless / self-hosted). Execs the sibling
182
+ /// `nmem-server` binary. Compatibility shim for systemd units written by the
183
+ /// old Python `nmem service install` (`ExecStart=… nmem serve --host … --port …`),
184
+ /// which broke when the Rust CLI — a REST client with no built-in server —
185
+ /// took over `/usr/local/bin/nmem`.
186
+ Serve {
187
+ /// Bind host (maps to NMEM_SERVER_ADDR host).
188
+ #[arg(long, default_value = "127.0.0.1")]
189
+ host: String,
190
+ /// Bind port (maps to NMEM_SERVER_ADDR port).
191
+ #[arg(long, default_value_t = 14242)]
192
+ port: u16,
193
+ },
181
194
  }
182
195
 
183
196
  // ── skills ───────────────────────────────────────────────────────────────────
@@ -1403,7 +1416,8 @@ pub struct ThreadImportArgs {
1403
1416
 
1404
1417
  #[derive(Args, Debug)]
1405
1418
  pub struct ThreadSaveArgs {
1406
- /// Source app: claude-code, codex, cursor, gemini-cli, opencode, pi, hermes.
1419
+ /// Source app: claude-code, codex, cursor, gemini-cli, grok, hermes,
1420
+ /// kimi-code, kimi-work, mimo-code, omp, opencode, pi.
1407
1421
  #[arg(long = "from", value_name = "HOST")]
1408
1422
  pub source_app: String,
1409
1423
  /// Project directory path (default: current dir).
@@ -1439,7 +1453,8 @@ pub struct ThreadSaveArgs {
1439
1453
 
1440
1454
  #[derive(Args, Debug)]
1441
1455
  pub struct ThreadSyncArgs {
1442
- /// Source app: claude-code, codex, cursor, gemini-cli, opencode, pi, hermes.
1456
+ /// Source app: claude-code, codex, cursor, gemini-cli, grok, hermes,
1457
+ /// kimi-code, kimi-work, mimo-code, omp, opencode, pi.
1443
1458
  #[arg(long = "from", value_name = "HOST")]
1444
1459
  pub source_app: String,
1445
1460
  /// Project directory path.
@@ -1457,7 +1472,8 @@ pub struct ThreadSyncArgs {
1457
1472
  /// Specific session ID.
1458
1473
  #[arg(long = "session-id")]
1459
1474
  pub session_id: Option<String>,
1460
- /// Cursor/Pi session directory/JSONL file or Hermes state.db to scan (repeatable).
1475
+ /// Host transcript directory/file to scan (repeatable). Supports sources
1476
+ /// such as Cursor workspaces, Pi/OMP JSONL roots, and Hermes state.db.
1461
1477
  #[arg(long = "session-dir")]
1462
1478
  pub session_dirs: Vec<String>,
1463
1479
  /// Truncate large tool results where supported.
@@ -45,6 +45,11 @@ async fn run(cli: Cli) -> anyhow::Result<()> {
45
45
  return launch_tui(api_flag, passthrough);
46
46
  }
47
47
 
48
+ // `serve` execs the backend (`nmem-server`) and needs no live client.
49
+ if let Command::Serve { host, port } = &cli.command {
50
+ return launch_server(host, *port);
51
+ }
52
+
48
53
  // Local-only config verbs (show/set/get-url/mcp show) need no live client.
49
54
  // Provider verbs hit /remote-llm/* and fall through to the client path.
50
55
  if let Command::Config { action } = &cli.command {
@@ -400,6 +405,7 @@ async fn run(cli: Cli) -> anyhow::Result<()> {
400
405
 
401
406
  // Handled before the client is built.
402
407
  Command::Tui { .. } => unreachable!("tui handled before client init"),
408
+ Command::Serve { .. } => unreachable!("serve handled before client init"),
403
409
  }
404
410
  }
405
411
 
@@ -440,11 +446,185 @@ fn launch_tui(api_flag: Option<&str>, passthrough: &[String]) -> anyhow::Result<
440
446
  std::process::exit(status.code().unwrap_or(1));
441
447
  }
442
448
 
449
+ /// Run the backend by exec'ing the sibling `nmem-server` binary (resolved next
450
+ /// to `nmem`, like the TUI). `nmem-server` takes its bind address from
451
+ /// `NMEM_SERVER_ADDR`, so map `--host`/`--port` onto that (only if the caller
452
+ /// hasn't already set it). On Unix we `exec` (replace the process) so systemd
453
+ /// tracks the server's own PID instead of a wrapper; on Windows we spawn and
454
+ /// forward the exit code. Compatibility shim for legacy `nmem serve …` units.
455
+ fn launch_server(host: &str, port: u16) -> anyhow::Result<()> {
456
+ let exe = std::env::current_exe()
457
+ .map_err(|e| anyhow::anyhow!("cannot resolve current executable: {e}"))?;
458
+ let dir = exe
459
+ .parent()
460
+ .ok_or_else(|| anyhow::anyhow!("cannot resolve executable directory"))?;
461
+ let bin_name = if cfg!(target_os = "windows") {
462
+ "nmem-server.exe"
463
+ } else {
464
+ "nmem-server"
465
+ };
466
+ let server_path = dir.join(bin_name);
467
+ if !server_path.exists() {
468
+ anyhow::bail!(
469
+ "the backend binary `{}` was not found next to `nmem` (looked in {}). \
470
+ `nmem serve` runs the bundled backend; install the full package, not just the CLI.",
471
+ bin_name,
472
+ dir.display()
473
+ );
474
+ }
475
+
476
+ let mut cmd = std::process::Command::new(&server_path);
477
+ // Respect an explicit NMEM_SERVER_ADDR; otherwise derive it from --host/--port.
478
+ if std::env::var_os("NMEM_SERVER_ADDR").is_none() {
479
+ cmd.env("NMEM_SERVER_ADDR", format!("{host}:{port}"));
480
+ }
481
+ // Derive a default NMEM_APP_DATA when unset, so a LEGACY systemd unit (the
482
+ // Python `nmem service install` generated `nmem.service` sets only HOME/XDG_*
483
+ // and calls `nmem serve`) still finds the user's existing graph. Without this
484
+ // the server's `apply_app_data_root` no-ops and it boots WITHOUT a writable
485
+ // graph/search/content surface. Explicit env always wins (docker/operators/the
486
+ // Tauri launcher set it). The canonical graph dir is `<platform-data>/NowledgeGraph`
487
+ // (identical in the Tauri launcher `setup_user_directory` and the Python
488
+ // `get_nowledge_graph_data_dir`); it IS the dir holding `nowledge_graph_v2.db`,
489
+ // matching the NMEM_APP_DATA contract in the server's `apply_app_data_root`.
490
+ if std::env::var_os("NMEM_APP_DATA").is_none() {
491
+ match default_app_data_dir() {
492
+ Some(dir) => {
493
+ cmd.env("NMEM_APP_DATA", &dir);
494
+ }
495
+ None => {
496
+ // Home unresolvable: never guess a path that could boot against an
497
+ // empty dir — tell the operator to set it explicitly instead.
498
+ eprintln!(
499
+ "nmem serve: NMEM_APP_DATA is unset and the home directory could not be \
500
+ resolved; set NMEM_APP_DATA to your NowledgeGraph data dir so the server \
501
+ serves your existing data."
502
+ );
503
+ }
504
+ }
505
+ }
506
+ // Config root: the legacy Python CLI stored app-settings.json / remote_llm.json
507
+ // / license under the platform CONFIG dir (XDG_CONFIG_HOME / %APPDATA% /
508
+ // Application Support). Without NMEM_APP_CONFIG_DIR the server's
509
+ // apply_app_data_root pins it to the GRAPH sibling instead, so an upgraded
510
+ // legacy headless install gets its graph back but loses provider, license, and
511
+ // access settings. Set the platform config dir when unset (explicit wins).
512
+ if std::env::var_os("NMEM_APP_CONFIG_DIR").is_none() {
513
+ if let Some(cfg) = default_config_dir() {
514
+ cmd.env("NMEM_APP_CONFIG_DIR", &cfg);
515
+ }
516
+ }
517
+ // Headless marker: `nmem serve` on a non-loopback bind (LAN / 0.0.0.0) is a
518
+ // DELIBERATE headless server (matching the Python image's `serve --host
519
+ // 0.0.0.0`). Mark it headless so the server's off-loopback fail-closed guard
520
+ // WARNS instead of REFUSING — otherwise an upgraded systemd/LAN start exits
521
+ // with the unauthenticated-bind error. Explicit NOWLEDGE_HEADLESS wins; a
522
+ // loopback bind stays unmarked (the desktop/local default).
523
+ if std::env::var_os("NOWLEDGE_HEADLESS").is_none() && !host_is_loopback(host) {
524
+ cmd.env("NOWLEDGE_HEADLESS", "1");
525
+ }
526
+
527
+ #[cfg(unix)]
528
+ {
529
+ use std::os::unix::process::CommandExt;
530
+ // exec replaces this process; only returns on failure.
531
+ let err = cmd.exec();
532
+ Err(anyhow::anyhow!(
533
+ "failed to exec {}: {err}",
534
+ server_path.display()
535
+ ))
536
+ }
537
+ #[cfg(not(unix))]
538
+ {
539
+ let status = cmd
540
+ .status()
541
+ .map_err(|e| anyhow::anyhow!("failed to launch {}: {e}", server_path.display()))?;
542
+ std::process::exit(status.code().unwrap_or(1));
543
+ }
544
+ }
545
+
546
+ /// The canonical NowledgeGraph graph DATA dir for the current platform, or
547
+ /// `None` when the home directory cannot be resolved. Identical to the Tauri
548
+ /// launcher `setup_user_directory` + the Python `get_nowledge_graph_data_dir`,
549
+ /// so a default-derived `NMEM_APP_DATA` points at the SAME dir the desktop/Python
550
+ /// already wrote (never an empty new one):
551
+ /// - macOS: `~/Library/Application Support/NowledgeGraph`
552
+ /// - Windows: `%LOCALAPPDATA%/NowledgeGraph` (fallback `~/AppData/Local/...`)
553
+ /// - Linux: `$XDG_DATA_HOME/NowledgeGraph` (fallback `~/.local/share/...`)
554
+ fn default_app_data_dir() -> Option<std::path::PathBuf> {
555
+ let home = std::env::var_os("HOME")
556
+ .or_else(|| std::env::var_os("USERPROFILE"))
557
+ .map(std::path::PathBuf::from)
558
+ .filter(|p| !p.as_os_str().is_empty())?;
559
+ Some(default_app_data_dir_from(&home))
560
+ }
561
+
562
+ /// Pure, testable core of [`default_app_data_dir`] (home injected).
563
+ fn default_app_data_dir_from(home: &std::path::Path) -> std::path::PathBuf {
564
+ let base = if cfg!(target_os = "macos") {
565
+ home.join("Library").join("Application Support")
566
+ } else if cfg!(target_os = "windows") {
567
+ std::env::var_os("LOCALAPPDATA")
568
+ .map(std::path::PathBuf::from)
569
+ .filter(|p| !p.as_os_str().is_empty())
570
+ .unwrap_or_else(|| home.join("AppData").join("Local"))
571
+ } else {
572
+ std::env::var_os("XDG_DATA_HOME")
573
+ .map(std::path::PathBuf::from)
574
+ .filter(|p| !p.as_os_str().is_empty())
575
+ .unwrap_or_else(|| home.join(".local").join("share"))
576
+ };
577
+ base.join("NowledgeGraph")
578
+ }
579
+
580
+ /// The platform CONFIG dir `co.nowledge.mem.desktop` (app-settings / remote_llm /
581
+ /// license / device files), mirroring the server's `app_desktop_dir` + Python's
582
+ /// `get_app_config_dir`. `None` when home cannot be resolved.
583
+ /// - macOS: `~/Library/Application Support/co.nowledge.mem.desktop`
584
+ /// - Windows: `%APPDATA%/co.nowledge.mem.desktop` (Roaming; fallback `~/AppData/Roaming`)
585
+ /// - Linux: `$XDG_CONFIG_HOME/co.nowledge.mem.desktop` (fallback `~/.config`)
586
+ fn default_config_dir() -> Option<std::path::PathBuf> {
587
+ let home = std::env::var_os("HOME")
588
+ .or_else(|| std::env::var_os("USERPROFILE"))
589
+ .map(std::path::PathBuf::from)
590
+ .filter(|p| !p.as_os_str().is_empty())?;
591
+ let base = if cfg!(target_os = "macos") {
592
+ home.join("Library").join("Application Support")
593
+ } else if cfg!(target_os = "windows") {
594
+ std::env::var_os("APPDATA")
595
+ .map(std::path::PathBuf::from)
596
+ .filter(|p| !p.as_os_str().is_empty())
597
+ .unwrap_or_else(|| home.join("AppData").join("Roaming"))
598
+ } else {
599
+ std::env::var_os("XDG_CONFIG_HOME")
600
+ .map(std::path::PathBuf::from)
601
+ .filter(|p| !p.as_os_str().is_empty())
602
+ .unwrap_or_else(|| home.join(".config"))
603
+ };
604
+ Some(base.join("co.nowledge.mem.desktop"))
605
+ }
606
+
607
+ /// True when `host` is a loopback bind (the server only refuses an
608
+ /// off-loopback, unauthenticated bind — loopback never needs the headless mark).
609
+ fn host_is_loopback(host: &str) -> bool {
610
+ matches!(host, "127.0.0.1" | "localhost" | "::1" | "[::1]")
611
+ || host.starts_with("127.")
612
+ || host.eq_ignore_ascii_case("localhost")
613
+ }
614
+
443
615
  #[cfg(test)]
444
616
  mod arg_tests {
445
617
  use super::*;
446
618
  use clap::Parser;
447
619
 
620
+ #[test]
621
+ fn default_app_data_dir_ends_in_nowledge_graph() {
622
+ let home = std::path::Path::new("/home/tester");
623
+ let dir = default_app_data_dir_from(home);
624
+ assert!(dir.ends_with("NowledgeGraph"), "must be the NowledgeGraph graph dir: {dir:?}");
625
+ assert!(dir.starts_with(home), "must hang off the given home: {dir:?}");
626
+ }
627
+
448
628
  #[test]
449
629
  fn parses_global_api_and_json() {
450
630
  let cli =
@@ -13,7 +13,7 @@ build-backend = "maturin"
13
13
 
14
14
  [project]
15
15
  name = "nmem-cli"
16
- version = "0.10.4"
16
+ version = "0.10.6"
17
17
  description = "CLI and TUI for Nowledge Mem - AI memory management"
18
18
  authors = [
19
19
  { name = "Nowledge Labs" },
File without changes