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.
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/PKG-INFO +1 -1
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/pyproject.toml +1 -1
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/__init__.py +1 -1
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/cli.py +152 -4
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/session_import.py +90 -23
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/.gitignore +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/README.md +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/agent_profiles.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/claude_paths.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/data_transfer_paths.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/guidance_rules.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/kfs_cli.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/license_payload.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/memories_reclassify.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/memory_relations_cli.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/py.typed +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/skills_cli.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/__init__.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/__main__.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/api_client.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/app.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/__init__.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/dashboard.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/graph.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/help.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/memories.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/memory_detail.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/settings.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/thread_detail.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/screens/threads.py +0 -0
- {nmem_cli-0.9.22 → nmem_cli-0.9.24}/src/nmem_cli/tui/widgets/__init__.py +0 -0
|
@@ -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 = {
|
|
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),
|
|
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
|
|
234
|
-
candidates = _discover_kimi_sessions(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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"
|
|
1300
|
-
"source_app":
|
|
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"
|
|
1336
|
-
"source_app":
|
|
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"
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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"
|
|
2626
|
+
title = f"{display_name} session - {os.path.basename(cwd.rstrip('/' + chr(92)))}"
|
|
2578
2627
|
else:
|
|
2579
|
-
title = f"
|
|
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"
|
|
2634
|
+
"thread_id": f"{source_app}-{sanitized_session_id}",
|
|
2585
2635
|
"title": title,
|
|
2586
2636
|
"messages": messages,
|
|
2587
|
-
"source":
|
|
2637
|
+
"source": source_app,
|
|
2588
2638
|
"project": project_name,
|
|
2589
2639
|
"workspace": cwd,
|
|
2590
2640
|
"metadata": {
|
|
2591
2641
|
"session_id": session_id,
|
|
2592
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|