nmem-cli 0.9.22__tar.gz → 0.9.24__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 (31) hide show
  1. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/PKG-INFO +1 -1
  2. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/pyproject.toml +1 -1
  3. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/__init__.py +1 -1
  4. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/cli.py +152 -4
  5. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/session_import.py +90 -23
  6. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/.gitignore +0 -0
  7. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/README.md +0 -0
  8. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/agent_profiles.py +0 -0
  9. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/claude_paths.py +0 -0
  10. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/data_transfer_paths.py +0 -0
  11. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/guidance_rules.py +0 -0
  12. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/kfs_cli.py +0 -0
  13. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/license_payload.py +0 -0
  14. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/memories_reclassify.py +0 -0
  15. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/memory_relations_cli.py +0 -0
  16. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/py.typed +0 -0
  17. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/skills_cli.py +0 -0
  18. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/__init__.py +0 -0
  19. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/__main__.py +0 -0
  20. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/api_client.py +0 -0
  21. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/app.py +0 -0
  22. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/__init__.py +0 -0
  23. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/dashboard.py +0 -0
  24. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/graph.py +0 -0
  25. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/help.py +0 -0
  26. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/memories.py +0 -0
  27. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
  28. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/settings.py +0 -0
  29. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
  30. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/threads.py +0 -0
  31. {nmem_cli-0.9.22 → nmem_cli-0.9.24}/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.9.22
3
+ Version: 0.9.24
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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nmem-cli"
3
- version = "0.9.22"
3
+ version = "0.9.24"
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.9.22"
23
+ __version__ = "0.9.24"
24
24
  __author__ = "Nowledge Labs"
25
25
 
26
26
  from .cli import main
@@ -4517,6 +4517,67 @@ def _parsed_threads_to_import_items(threads: Any) -> list[dict[str, Any]]:
4517
4517
  return items
4518
4518
 
4519
4519
 
4520
+ def _thread_authorship_metadata(
4521
+ *,
4522
+ agent_id: str | None = None,
4523
+ source_app: str | None = None,
4524
+ host_agent_id: str | None = None,
4525
+ ) -> dict[str, str]:
4526
+ """Resolve explicit thread authorship metadata from args/env.
4527
+
4528
+ `source_app` is provenance only. `agent_id` is the portable AI Identity.
4529
+ `host_agent_id` is an advanced external alias used by integrations; the CLI
4530
+ never invents it and never turns source_app into identity.
4531
+ """
4532
+ resolved_agent_id = (agent_id or os.environ.get("NMEM_AGENT_ID") or "").strip()
4533
+ resolved_host_agent_id = (
4534
+ host_agent_id or os.environ.get("NMEM_HOST_AGENT_ID") or ""
4535
+ ).strip()
4536
+ resolved_source_app = (source_app or "").strip()
4537
+
4538
+ metadata: dict[str, str] = {}
4539
+ if resolved_agent_id:
4540
+ metadata["agent_id"] = resolved_agent_id
4541
+ if resolved_source_app:
4542
+ metadata["source_app"] = resolved_source_app
4543
+ if resolved_host_agent_id:
4544
+ metadata["host_agent_id"] = resolved_host_agent_id
4545
+ return metadata
4546
+
4547
+
4548
+ def _merge_thread_authorship_metadata(
4549
+ target: dict[str, Any],
4550
+ authorship_metadata: dict[str, str],
4551
+ ) -> None:
4552
+ if not authorship_metadata:
4553
+ return
4554
+ metadata = target.get("metadata")
4555
+ if not isinstance(metadata, dict):
4556
+ metadata = {}
4557
+ metadata.update(authorship_metadata)
4558
+ target["metadata"] = metadata
4559
+
4560
+
4561
+ def _backfill_thread_authorship_metadata(
4562
+ thread_id: str,
4563
+ authorship_metadata: dict[str, str],
4564
+ *,
4565
+ space_id: str | None = None,
4566
+ ) -> None:
4567
+ if not authorship_metadata:
4568
+ return
4569
+ payload: dict[str, Any] = {
4570
+ "metadata": authorship_metadata,
4571
+ "only_missing": True,
4572
+ }
4573
+ if space_id:
4574
+ payload["space_id"] = space_id
4575
+ api_post_optional(
4576
+ f"/threads/{quote(thread_id, safe='')}/metadata/merge",
4577
+ payload,
4578
+ )
4579
+
4580
+
4520
4581
  def cmd_threads_import(
4521
4582
  file: str | None = None,
4522
4583
  messages_json: str | None = None,
@@ -4525,10 +4586,17 @@ def cmd_threads_import(
4525
4586
  source: str | None = None,
4526
4587
  from_stdin: bool = False,
4527
4588
  space_id: str | None = None,
4589
+ agent_id: str | None = None,
4590
+ host_agent_id: str | None = None,
4528
4591
  ) -> None:
4529
4592
  """Import a thread from file, JSON messages, or stdin."""
4530
4593
  payload: dict[str, Any] = {}
4531
4594
  claude_pending_memories: list[Any] | None = None
4595
+ authorship_metadata = _thread_authorship_metadata(
4596
+ agent_id=agent_id,
4597
+ source_app=source,
4598
+ host_agent_id=host_agent_id,
4599
+ )
4532
4600
 
4533
4601
  if thread_id:
4534
4602
  payload["thread_id"] = thread_id
@@ -4704,6 +4772,15 @@ def cmd_threads_import(
4704
4772
  print_error("Missing Input", "Provide --file, --messages, or --stdin")
4705
4773
  sys.exit(1)
4706
4774
 
4775
+ if authorship_metadata:
4776
+ threads = payload.get("threads")
4777
+ if isinstance(threads, list):
4778
+ for item in threads:
4779
+ if isinstance(item, dict):
4780
+ _merge_thread_authorship_metadata(item, authorship_metadata)
4781
+ else:
4782
+ _merge_thread_authorship_metadata(payload, authorship_metadata)
4783
+
4707
4784
  if not is_json_mode():
4708
4785
  with Progress(
4709
4786
  SpinnerColumn(),
@@ -5129,6 +5206,8 @@ def cmd_threads_save(
5129
5206
  summary: str | None = None,
5130
5207
  truncate: bool = False,
5131
5208
  space_id: str | None = None,
5209
+ agent_id: str | None = None,
5210
+ host_agent_id: str | None = None,
5132
5211
  ) -> None:
5133
5212
  """Save coding session(s) as conversation thread(s).
5134
5213
 
@@ -5163,6 +5242,12 @@ def cmd_threads_save(
5163
5242
  print_error("Path Not Found", str(resolved_path))
5164
5243
  sys.exit(1)
5165
5244
 
5245
+ authorship_metadata = _thread_authorship_metadata(
5246
+ agent_id=agent_id,
5247
+ source_app=client,
5248
+ host_agent_id=host_agent_id,
5249
+ )
5250
+
5166
5251
  def _save_sessions() -> dict[str, Any]:
5167
5252
  try:
5168
5253
  sessions = discover_sessions(client, str(resolved_path), mode, session_id)
@@ -5197,6 +5282,7 @@ def cmd_threads_save(
5197
5282
  }
5198
5283
 
5199
5284
  metadata = dict(thread_data.get("metadata") or {})
5285
+ metadata.update(authorship_metadata)
5200
5286
  if index == 0 and summary:
5201
5287
  metadata["user_summary"] = summary
5202
5288
 
@@ -5230,6 +5316,11 @@ def cmd_threads_save(
5230
5316
  }
5231
5317
  )
5232
5318
  else:
5319
+ _backfill_thread_authorship_metadata(
5320
+ thread_id,
5321
+ authorship_metadata,
5322
+ space_id=space_id,
5323
+ )
5233
5324
  append_data = api_post(
5234
5325
  f"/threads/{quote(thread_id, safe='')}/append",
5235
5326
  {
@@ -5311,6 +5402,7 @@ def _write_thread_from_session_data(
5311
5402
  source_app: str,
5312
5403
  space_id: str | None,
5313
5404
  metadata_updates: dict[str, Any] | None = None,
5405
+ authorship_metadata: dict[str, str] | None = None,
5314
5406
  idempotency_prefix: str | None = None,
5315
5407
  ) -> dict[str, Any]:
5316
5408
  external_id_keys = (
@@ -5408,6 +5500,8 @@ def _write_thread_from_session_data(
5408
5500
  metadata = dict(thread_data.get("metadata") or {})
5409
5501
  if metadata_updates:
5410
5502
  metadata.update(metadata_updates)
5503
+ if authorship_metadata:
5504
+ metadata.update(authorship_metadata)
5411
5505
 
5412
5506
  existing_params = {"space_id": space_id} if space_id else None
5413
5507
  existing_thread = api_get_optional(
@@ -5445,6 +5539,11 @@ def _write_thread_from_session_data(
5445
5539
  append_payload.update(metadata_updates)
5446
5540
  if idempotency_prefix:
5447
5541
  append_payload["idempotency_key"] = f"{idempotency_prefix}:{session_id}"
5542
+ _backfill_thread_authorship_metadata(
5543
+ thread_id,
5544
+ authorship_metadata or {},
5545
+ space_id=space_id,
5546
+ )
5448
5547
  append_data = api_post(
5449
5548
  f"/threads/{quote(thread_id, safe='')}/append",
5450
5549
  append_payload,
@@ -5519,6 +5618,8 @@ def cmd_threads_sync(
5519
5618
  truncate: bool = False,
5520
5619
  space_id: str | None = None,
5521
5620
  source_roots: list[str] | None = None,
5621
+ agent_id: str | None = None,
5622
+ host_agent_id: str | None = None,
5522
5623
  ) -> None:
5523
5624
  """Preview or import historical sessions for a supported local agent host."""
5524
5625
  if client not in SUPPORTED_SESSION_CLIENTS:
@@ -5548,7 +5649,14 @@ def cmd_threads_sync(
5548
5649
  # These clients have one local session store, so they can safely preview all
5549
5650
  # sessions by default. Cursor stays cwd-scoped unless the caller gives an
5550
5651
  # explicit transcript root; then the root itself is the user's selected scope.
5551
- default_all_projects_clients = {"hermes", "kimi-code", "mimo-code", "omp", "pi"}
5652
+ default_all_projects_clients = {
5653
+ "hermes",
5654
+ "kimi-code",
5655
+ "kimi-work",
5656
+ "mimo-code",
5657
+ "omp",
5658
+ "pi",
5659
+ }
5552
5660
  discovery_path = project_path
5553
5661
  if all_projects:
5554
5662
  discovery_path = "__all__"
@@ -5571,6 +5679,12 @@ def cmd_threads_sync(
5571
5679
  print_error("Path Not Found", str(resolved_path))
5572
5680
  sys.exit(1)
5573
5681
 
5682
+ authorship_metadata = _thread_authorship_metadata(
5683
+ agent_id=agent_id,
5684
+ source_app=client,
5685
+ host_agent_id=host_agent_id,
5686
+ )
5687
+
5574
5688
  try:
5575
5689
  sessions = discover_sessions(
5576
5690
  client,
@@ -5643,6 +5757,7 @@ def cmd_threads_sync(
5643
5757
  "analysis": "searchable-now-distill-on-demand",
5644
5758
  "sync_reason": "history_sync",
5645
5759
  },
5760
+ authorship_metadata=authorship_metadata,
5646
5761
  idempotency_prefix=f"{client}:history",
5647
5762
  )
5648
5763
  )
@@ -9782,6 +9897,7 @@ _MCP_HOST_APPS = {
9782
9897
  "claude-code": "Claude Code",
9783
9898
  "claude-desktop": "Claude",
9784
9899
  "kimi-code": "Kimi Code",
9900
+ "kimi-work": "Kimi Work",
9785
9901
  "zcode": "ZCode",
9786
9902
  "mimo-code": "MiMo Code",
9787
9903
  "omp": "OMP",
@@ -10737,6 +10853,14 @@ PRIORITY
10737
10853
  action="store_true",
10738
10854
  help="Read conversation markdown from stdin",
10739
10855
  )
10856
+ imp.add_argument(
10857
+ "--agent-id",
10858
+ help="Explicit Nowledge AI Identity id/slug for imported threads",
10859
+ )
10860
+ imp.add_argument(
10861
+ "--host-agent-id",
10862
+ help=argparse.SUPPRESS,
10863
+ )
10740
10864
 
10741
10865
  a = thr_subs.add_parser(
10742
10866
  "append",
@@ -10839,7 +10963,7 @@ PRIORITY
10839
10963
  sv = thr_subs.add_parser(
10840
10964
  "save",
10841
10965
  help=(
10842
- "Save Claude Code, Codex, Cursor, Gemini CLI, Kimi Code, "
10966
+ "Save Claude Code, Codex, Cursor, Gemini CLI, Kimi Code/Kimi Work, "
10843
10967
  "OpenCode-family, Pi/OMP, or Hermes session as thread"
10844
10968
  ),
10845
10969
  parents=[_space_parent],
@@ -10849,6 +10973,7 @@ PRIORITY
10849
10973
  nmem t save --from codex --session-id sess_abc123
10850
10974
  nmem t save --from cursor -p /path/to/project
10851
10975
  nmem t save --from kimi-code --session-id sess_abc123
10976
+ nmem t save --from kimi-work --session-id conv_abc123
10852
10977
  nmem t save --from mimo-code --session-id ses_abc123
10853
10978
  nmem t save --from omp -p /path/to/project
10854
10979
  nmem t save --from opencode -p /path/to/project
@@ -10887,6 +11012,14 @@ PRIORITY
10887
11012
  action="store_true",
10888
11013
  help="Truncate large tool results (>10KB)",
10889
11014
  )
11015
+ sv.add_argument(
11016
+ "--agent-id",
11017
+ help="Explicit Nowledge AI Identity id/slug for saved sessions",
11018
+ )
11019
+ sv.add_argument(
11020
+ "--host-agent-id",
11021
+ help=argparse.SUPPRESS,
11022
+ )
10890
11023
 
10891
11024
  sync = thr_subs.add_parser(
10892
11025
  "sync",
@@ -10896,6 +11029,7 @@ PRIORITY
10896
11029
  nmem t sync --from pi
10897
11030
  nmem t sync --from pi --apply
10898
11031
  nmem t sync --from kimi-code --apply
11032
+ nmem t sync --from kimi-work --apply
10899
11033
  nmem t sync --from mimo-code --apply
10900
11034
  nmem t sync --from omp --apply
10901
11035
  nmem t sync --from cursor --all-projects --limit 20
@@ -10920,8 +11054,8 @@ PRIORITY
10920
11054
  default=None,
10921
11055
  help=(
10922
11056
  "Project directory path. Defaults to all sessions for global-store "
10923
- "hosts (Hermes, Kimi Code, MiMo Code, OMP, Pi), and current "
10924
- "directory for other clients."
11057
+ "hosts (Hermes, Kimi Code, Kimi Work, MiMo Code, OMP, Pi), "
11058
+ "and current directory for other clients."
10925
11059
  ),
10926
11060
  )
10927
11061
  sync.add_argument(
@@ -10951,6 +11085,14 @@ PRIORITY
10951
11085
  action="store_true",
10952
11086
  help="Truncate large tool results where supported",
10953
11087
  )
11088
+ sync.add_argument(
11089
+ "--agent-id",
11090
+ help="Explicit Nowledge AI Identity id/slug for synced sessions",
11091
+ )
11092
+ sync.add_argument(
11093
+ "--host-agent-id",
11094
+ help=argparse.SUPPRESS,
11095
+ )
10954
11096
 
10955
11097
  # triage - Check if conversation is worth distilling
10956
11098
  tr = thr_subs.add_parser(
@@ -12101,6 +12243,8 @@ def main() -> int:
12101
12243
  source=getattr(args, "source", None),
12102
12244
  from_stdin=getattr(args, "stdin", False),
12103
12245
  space_id=getattr(args, "space", None),
12246
+ agent_id=getattr(args, "agent_id", None),
12247
+ host_agent_id=getattr(args, "host_agent_id", None),
12104
12248
  )
12105
12249
  elif action == "append":
12106
12250
  cmd_threads_append(
@@ -12140,6 +12284,8 @@ def main() -> int:
12140
12284
  summary=getattr(args, "summary", None),
12141
12285
  truncate=getattr(args, "truncate", False),
12142
12286
  space_id=getattr(args, "space", None),
12287
+ agent_id=getattr(args, "agent_id", None),
12288
+ host_agent_id=getattr(args, "host_agent_id", None),
12143
12289
  )
12144
12290
  elif action == "sync":
12145
12291
  cmd_threads_sync(
@@ -12152,6 +12298,8 @@ def main() -> int:
12152
12298
  truncate=getattr(args, "truncate", False),
12153
12299
  space_id=getattr(args, "space", None),
12154
12300
  source_roots=getattr(args, "session_dirs", None),
12301
+ agent_id=getattr(args, "agent_id", None),
12302
+ host_agent_id=getattr(args, "host_agent_id", None),
12155
12303
  )
12156
12304
  elif action == "triage":
12157
12305
  cmd_threads_triage(
@@ -7,7 +7,7 @@ without importing server internals.
7
7
 
8
8
  Maintenance rule:
9
9
  - if Claude Code, Codex/Cursor/Grok, Gemini CLI, OpenCode-family agents,
10
- Kimi Code, Pi/OMP, or Hermes change their transcript formats or storage
10
+ Kimi Code/Kimi Work, Pi/OMP, or Hermes change their transcript formats or storage
11
11
  layout, update this client module and the corresponding server-local
12
12
  parser/import path in the same change
13
13
  - keep the normalized thread output aligned across both sides
@@ -38,6 +38,7 @@ SUPPORTED_SESSION_CLIENTS = (
38
38
  "grok",
39
39
  "gemini-cli",
40
40
  "kimi-code",
41
+ "kimi-work",
41
42
  "mimo-code",
42
43
  "omp",
43
44
  "opencode",
@@ -230,8 +231,13 @@ def discover_sessions(
230
231
  candidates = _discover_grok_sessions(project_path, session_id)
231
232
  elif client == "gemini-cli":
232
233
  candidates = _discover_gemini_sessions(project_path, session_id)
233
- elif client == "kimi-code":
234
- candidates = _discover_kimi_sessions(project_path, session_id, source_roots)
234
+ elif client in {"kimi-code", "kimi-work"}:
235
+ candidates = _discover_kimi_sessions(
236
+ project_path,
237
+ session_id,
238
+ source_roots,
239
+ source_app=client,
240
+ )
235
241
  elif client == "mimo-code":
236
242
  candidates = _discover_mimo_code_sessions(project_path, session_id, source_roots)
237
243
  elif client == "omp":
@@ -271,6 +277,8 @@ def parse_session(
271
277
  return parse_gemini_session(file_path)
272
278
  if client == "kimi-code":
273
279
  return parse_kimi_code_session(file_path)
280
+ if client == "kimi-work":
281
+ return parse_kimi_work_session(file_path)
274
282
  if client == "mimo-code":
275
283
  return parse_mimo_code_session(file_path, session_id=session_id)
276
284
  if client == "omp":
@@ -1134,10 +1142,32 @@ def _discover_mimo_code_sessions(
1134
1142
  return candidates
1135
1143
 
1136
1144
 
1137
- def _get_kimi_session_roots(source_roots: Optional[list[str]] = None) -> list[Path]:
1145
+ def _get_kimi_work_home() -> Path:
1146
+ return _env_path(
1147
+ "KIMI_WORK_HOME",
1148
+ Path.home()
1149
+ / "Library"
1150
+ / "Application Support"
1151
+ / "kimi-desktop"
1152
+ / "daimon-share"
1153
+ / "daimon"
1154
+ / "runtime"
1155
+ / "kimi-code"
1156
+ / "home",
1157
+ )
1158
+
1159
+
1160
+ def _get_kimi_session_roots(
1161
+ source_roots: Optional[list[str]] = None,
1162
+ *,
1163
+ source_app: str = "kimi-code",
1164
+ ) -> list[Path]:
1138
1165
  if source_roots:
1139
1166
  return [Path(root).expanduser() for root in source_roots]
1140
- kimi_home = _env_path("KIMI_CODE_HOME", Path.home() / ".kimi-code")
1167
+ if source_app == "kimi-work":
1168
+ kimi_home = _get_kimi_work_home()
1169
+ else:
1170
+ kimi_home = _env_path("KIMI_CODE_HOME", Path.home() / ".kimi-code")
1141
1171
  return [kimi_home / "sessions"]
1142
1172
 
1143
1173
 
@@ -1151,7 +1181,11 @@ def _kimi_session_dir_from_wire(wire_path: Path) -> Path:
1151
1181
  return wire_path.parent
1152
1182
 
1153
1183
 
1154
- def _iter_kimi_session_dirs(source_roots: Optional[list[str]] = None) -> list[Path]:
1184
+ def _iter_kimi_session_dirs(
1185
+ source_roots: Optional[list[str]] = None,
1186
+ *,
1187
+ source_app: str = "kimi-code",
1188
+ ) -> list[Path]:
1155
1189
  session_dirs: list[Path] = []
1156
1190
  seen: set[Path] = set()
1157
1191
 
@@ -1168,7 +1202,7 @@ def _iter_kimi_session_dirs(source_roots: Optional[list[str]] = None) -> list[Pa
1168
1202
  seen.add(resolved)
1169
1203
  session_dirs.append(candidate)
1170
1204
 
1171
- for root in _get_kimi_session_roots(source_roots):
1205
+ for root in _get_kimi_session_roots(source_roots, source_app=source_app):
1172
1206
  if not root.exists():
1173
1207
  continue
1174
1208
  if root.is_file() and root.name == "wire.jsonl":
@@ -1278,6 +1312,7 @@ def _kimi_messages_from_events(
1278
1312
  events: list[dict[str, Any]],
1279
1313
  *,
1280
1314
  session_id: str,
1315
+ source_app: str = "kimi-code",
1281
1316
  ) -> tuple[list[dict[str, Any]], Optional[str]]:
1282
1317
  messages: list[dict[str, Any]] = []
1283
1318
  first_user_content: Optional[str] = None
@@ -1296,8 +1331,8 @@ def _kimi_messages_from_events(
1296
1331
  "timestamp": assistant_timestamp,
1297
1332
  "metadata": {
1298
1333
  "external_id": assistant_external_id
1299
- or f"kimi-code-{session_id}-assistant-{len(messages)}",
1300
- "source_app": "kimi-code",
1334
+ or f"{source_app}-{session_id}-assistant-{len(messages)}",
1335
+ "source_app": source_app,
1301
1336
  },
1302
1337
  }
1303
1338
  )
@@ -1332,8 +1367,8 @@ def _kimi_messages_from_events(
1332
1367
  "content": content,
1333
1368
  "timestamp": timestamp,
1334
1369
  "metadata": {
1335
- "external_id": f"kimi-code-{session_id}-message-{index}",
1336
- "source_app": "kimi-code",
1370
+ "external_id": f"{source_app}-{session_id}-message-{index}",
1371
+ "source_app": source_app,
1337
1372
  "kimi_event_type": event_type,
1338
1373
  "kimi_message_role": role,
1339
1374
  },
@@ -1353,7 +1388,7 @@ def _kimi_messages_from_events(
1353
1388
  continue
1354
1389
  if not assistant_chunks:
1355
1390
  assistant_timestamp = timestamp
1356
- assistant_external_id = f"kimi-code-{session_id}-loop-{index}"
1391
+ assistant_external_id = f"{source_app}-{session_id}-loop-{index}"
1357
1392
  assistant_chunks.append(part_text)
1358
1393
  elif loop_type in {"step.end", "message.end", "turn.end"}:
1359
1394
  flush_assistant()
@@ -1366,6 +1401,8 @@ def _discover_kimi_sessions(
1366
1401
  project_path: str,
1367
1402
  target_session_id: Optional[str],
1368
1403
  source_roots: Optional[list[str]] = None,
1404
+ *,
1405
+ source_app: str = "kimi-code",
1369
1406
  ) -> list[SessionCandidate]:
1370
1407
  all_projects = _is_all_projects_path(project_path)
1371
1408
  normalized_project_path = (
@@ -1373,7 +1410,8 @@ def _discover_kimi_sessions(
1373
1410
  )
1374
1411
  candidates: list[SessionCandidate] = []
1375
1412
 
1376
- for session_dir in _iter_kimi_session_dirs(source_roots):
1413
+ display_name = "Kimi Work" if source_app == "kimi-work" else "Kimi Code"
1414
+ for session_dir in _iter_kimi_session_dirs(source_roots, source_app=source_app):
1377
1415
  try:
1378
1416
  session_id = session_dir.name
1379
1417
  if target_session_id and session_id != target_session_id:
@@ -1388,7 +1426,11 @@ def _discover_kimi_sessions(
1388
1426
  ):
1389
1427
  continue
1390
1428
 
1391
- messages, _ = _kimi_messages_from_events(events, session_id=session_id)
1429
+ messages, _ = _kimi_messages_from_events(
1430
+ events,
1431
+ session_id=session_id,
1432
+ source_app=source_app,
1433
+ )
1392
1434
  user_messages = sum(1 for msg in messages if msg["role"] == "user")
1393
1435
  assistant_messages = sum(
1394
1436
  1 for msg in messages if msg["role"] == "assistant"
@@ -1417,7 +1459,8 @@ def _discover_kimi_sessions(
1417
1459
  )
1418
1460
  except Exception as exc:
1419
1461
  logger.warning(
1420
- "Failed to analyze Kimi Code session %s: %s",
1462
+ "Failed to analyze %s session %s: %s",
1463
+ display_name,
1421
1464
  session_dir,
1422
1465
  exc,
1423
1466
  )
@@ -2544,7 +2587,12 @@ def parse_omp_session(file_path: Path) -> dict[str, Any]:
2544
2587
  }
2545
2588
 
2546
2589
 
2547
- def parse_kimi_code_session(session_path: Path) -> dict[str, Any]:
2590
+ def _parse_kimi_family_session(
2591
+ session_path: Path,
2592
+ *,
2593
+ source_app: str,
2594
+ display_name: str,
2595
+ ) -> dict[str, Any]:
2548
2596
  session_dir = (
2549
2597
  _kimi_session_dir_from_wire(session_path)
2550
2598
  if session_path.is_file()
@@ -2556,16 +2604,17 @@ def parse_kimi_code_session(session_path: Path) -> dict[str, Any]:
2556
2604
  messages, first_user_content = _kimi_messages_from_events(
2557
2605
  events,
2558
2606
  session_id=session_id,
2607
+ source_app=source_app,
2559
2608
  )
2560
2609
  if not messages:
2561
2610
  raise SessionImportError(
2562
- "No valid user/assistant messages found in Kimi Code session"
2611
+ f"No valid user/assistant messages found in {display_name} session"
2563
2612
  )
2564
2613
  if not any(msg["role"] == "user" for msg in messages) or not any(
2565
2614
  msg["role"] == "assistant" for msg in messages
2566
2615
  ):
2567
2616
  raise SessionImportError(
2568
- "Kimi Code session needs at least one user and one assistant message"
2617
+ f"{display_name} session needs at least one user and one assistant message"
2569
2618
  )
2570
2619
 
2571
2620
  cwd = _kimi_cwd_from_events(events) or _kimi_string(state.get("cwd"))
@@ -2574,22 +2623,24 @@ def parse_kimi_code_session(session_path: Path) -> dict[str, Any]:
2574
2623
  if first_user_content:
2575
2624
  title = first_user_content[:120].replace("\n", " ").strip()
2576
2625
  elif cwd:
2577
- title = f"Kimi Code session - {os.path.basename(cwd.rstrip('/' + chr(92)))}"
2626
+ title = f"{display_name} session - {os.path.basename(cwd.rstrip('/' + chr(92)))}"
2578
2627
  else:
2579
- title = f"Kimi Code session - {session_id}"
2628
+ title = f"{display_name} session - {session_id}"
2580
2629
 
2581
2630
  sanitized_session_id = re.sub(r"[^a-z0-9._-]+", "-", session_id.lower())
2582
2631
  project_name = os.path.basename(cwd.rstrip("/" + chr(92))) if cwd else None
2632
+ metadata_session_key = f"{source_app.replace('-', '_')}_session_id"
2583
2633
  return {
2584
- "thread_id": f"kimi-code-{sanitized_session_id}",
2634
+ "thread_id": f"{source_app}-{sanitized_session_id}",
2585
2635
  "title": title,
2586
2636
  "messages": messages,
2587
- "source": "kimi-code",
2637
+ "source": source_app,
2588
2638
  "project": project_name,
2589
2639
  "workspace": cwd,
2590
2640
  "metadata": {
2591
2641
  "session_id": session_id,
2592
- "kimi_code_session_id": session_id,
2642
+ metadata_session_key: session_id,
2643
+ "runtime_family": "kimi-code",
2593
2644
  "created_at": state.get("createdAt") or state.get("created_at"),
2594
2645
  "updated_at": state.get("updatedAt") or state.get("updated_at"),
2595
2646
  "last_prompt": state.get("lastPrompt"),
@@ -2600,6 +2651,22 @@ def parse_kimi_code_session(session_path: Path) -> dict[str, Any]:
2600
2651
  }
2601
2652
 
2602
2653
 
2654
+ def parse_kimi_code_session(session_path: Path) -> dict[str, Any]:
2655
+ return _parse_kimi_family_session(
2656
+ session_path,
2657
+ source_app="kimi-code",
2658
+ display_name="Kimi Code",
2659
+ )
2660
+
2661
+
2662
+ def parse_kimi_work_session(session_path: Path) -> dict[str, Any]:
2663
+ return _parse_kimi_family_session(
2664
+ session_path,
2665
+ source_app="kimi-work",
2666
+ display_name="Kimi Work",
2667
+ )
2668
+
2669
+
2603
2670
  def parse_hermes_session(
2604
2671
  db_path: Path,
2605
2672
  *,
File without changes
File without changes