memorysync-cli 1.2.1__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/PKG-INFO +1 -1
  2. memorysync_cli-1.3.0/src/memorysync_cli/_version.py +1 -0
  3. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/memory.py +80 -4
  4. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/registry.json +13 -3
  5. memorysync_cli-1.2.1/src/memorysync_cli/_version.py +0 -1
  6. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/.gitignore +0 -0
  7. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/LICENSE +0 -0
  8. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/README.md +0 -0
  9. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/pyproject.toml +0 -0
  10. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/__init__.py +0 -0
  11. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/__main__.py +0 -0
  12. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/args.py +0 -0
  13. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/__init__.py +0 -0
  14. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/admin.py +0 -0
  15. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/init.py +0 -0
  16. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/source.py +0 -0
  17. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/commands/tooling.py +0 -0
  18. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/completions.py +0 -0
  19. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/config.py +0 -0
  20. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/credentials.py +0 -0
  21. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/errors.py +0 -0
  22. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/evaluation.py +0 -0
  23. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/http.py +0 -0
  24. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/main.py +0 -0
  25. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/output.py +0 -0
  26. {memorysync_cli-1.2.1 → memorysync_cli-1.3.0}/src/memorysync_cli/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memorysync-cli
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: MemorySync from your terminal. Zero dependencies.
5
5
  Project-URL: Documentation, https://docs.memorysync.io/cli
6
6
  Project-URL: Homepage, https://memorysync.io/cli
@@ -0,0 +1 @@
1
+ __version__ = "1.3.0"
@@ -512,6 +512,8 @@ def import_memories(ctx: dict) -> dict:
512
512
  records: list[dict] = []
513
513
  errors: list[str] = []
514
514
 
515
+ ref_field = flags.get("ref_field") or None
516
+
515
517
  def _validate(number: int, entry: Any) -> None:
516
518
  """Record one candidate as either usable or an error, in line order."""
517
519
  if not isinstance(entry, dict):
@@ -528,6 +530,7 @@ def import_memories(ctx: dict) -> dict:
528
530
  "text": text.strip(),
529
531
  "source": entry.get("source") or "cli-import",
530
532
  "line": number,
533
+ "ref": _pick_ref(entry, ref_field),
531
534
  }
532
535
  if isinstance(entry.get("metadata"), dict):
533
536
  record["metadata"] = entry["metadata"]
@@ -564,12 +567,14 @@ def import_memories(ctx: dict) -> dict:
564
567
  if flags.get("dry_run"):
565
568
  preview = list(errors[:10])
566
569
  trailer = len(errors) - len(preview)
570
+ with_ref = sum(1 for record in records if record.get("ref"))
567
571
  return {
568
572
  "data": {
569
573
  "file": source_path,
570
574
  "valid": len(records),
571
575
  "invalid": len(errors),
572
576
  "errors": errors,
577
+ "identified": with_ref,
573
578
  },
574
579
  "text": lambda: "\n".join(
575
580
  line
@@ -577,6 +582,10 @@ def import_memories(ctx: dict) -> dict:
577
582
  style.yellow("Dry run. Nothing was sent."),
578
583
  f"{style.dim('valid ')} {len(records)}",
579
584
  f"{style.dim('invalid ')} {len(errors)}",
585
+ # Shown always, not only under --resume, because knowing the
586
+ # file has usable ids is what tells someone --resume is
587
+ # available to them.
588
+ f"{style.dim('with id ')} {with_ref}",
580
589
  *[style.dim(f" {message}") for message in preview],
581
590
  style.dim(f" ... and {trailer} more") if trailer > 0 else "",
582
591
  style.yellow(
@@ -599,6 +608,25 @@ def import_memories(ctx: dict) -> dict:
599
608
  if not records:
600
609
  raise usage_error(f"{source_path} contained no records.")
601
610
 
611
+ # --resume is only honest if every record has a stable identity. Falling back
612
+ # to the line number would break the moment the file is re-exported in a
613
+ # different order, and falling back to a hash of the text would make two
614
+ # genuinely identical records collide, so the second would be reported as
615
+ # already stored and silently dropped. Refusing names the problem instead.
616
+ if flags.get("resume"):
617
+ missing = [record for record in records if not record.get("ref")]
618
+ if missing:
619
+ shown = ", ".join(f"line {record['line']}" for record in missing[:5])
620
+ looked_for = f'"{ref_field}"' if ref_field else "id, _id, uuid"
621
+ extra = "" if ref_field else " and metadata.mem0_id"
622
+ raise usage_error(
623
+ f"--resume needs an id on every record, and {len(missing)} "
624
+ f"{'has' if len(missing) == 1 else 'have'} none.",
625
+ f"First: {shown}. Looked for {looked_for}{extra}. "
626
+ "Pass --ref-field <name> to use a different field, or run without "
627
+ "--resume.",
628
+ )
629
+
602
630
  batch_size = _resolve_batch_size(flags)
603
631
  batches = [records[i : i + batch_size] for i in range(0, len(records), batch_size)]
604
632
  totals = {
@@ -606,23 +634,31 @@ def import_memories(ctx: dict) -> dict:
606
634
  "imported": 0,
607
635
  "memory_ids": [],
608
636
  "skipped": 0,
637
+ # Rows the server had already stored under the same identifier. Counted
638
+ # apart from ``skipped`` because they mean something different to whoever
639
+ # is resuming: nothing was lost, this row is simply already in.
640
+ "already_imported": 0,
609
641
  "failed": 0,
610
642
  "unreported": 0,
611
643
  "failures": [],
612
644
  "batches": len(batches),
613
645
  "batch_size": batch_size,
646
+ "resume": bool(flags.get("resume")),
614
647
  }
615
648
 
649
+ resume = bool(flags.get("resume"))
616
650
  for batch in batches:
617
651
  before = totals["failed"]
618
- items = [
619
- {
652
+ items = []
653
+ for record in batch:
654
+ item = {
620
655
  key: value
621
656
  for key, value in record.items()
622
657
  if key in {"text", "source", "metadata"}
623
658
  }
624
- for record in batch
625
- ]
659
+ if resume and record.get("ref"):
660
+ item["client_ref"] = record["ref"]
661
+ items.append(item)
626
662
  try:
627
663
  response = api.bulk_add_memories({"items": items}) or {}
628
664
  except Exception as error: # noqa: BLE001 — reported, not raised
@@ -653,6 +689,11 @@ def import_memories(ctx: dict) -> dict:
653
689
  if status == "created":
654
690
  totals["imported"] += 1
655
691
  totals["memory_ids"].extend(item.get("memory_ids") or [])
692
+ elif status == "skipped" and item.get("reason") == "already_ingested":
693
+ totals["already_imported"] += 1
694
+ # The ids the first run produced, so a resumed import still ends
695
+ # up knowing every memory its file corresponds to.
696
+ totals["memory_ids"].extend(item.get("memory_ids") or [])
656
697
  elif status == "skipped":
657
698
  totals["skipped"] += 1
658
699
  else:
@@ -668,6 +709,11 @@ def import_memories(ctx: dict) -> dict:
668
709
  line
669
710
  for line in [
670
711
  f"{style.green('Imported')} {totals['imported']} of {totals['total']}",
712
+ style.dim(
713
+ f" {totals['already_imported']} already imported on an earlier run"
714
+ )
715
+ if totals["already_imported"]
716
+ else "",
671
717
  style.dim(f" {totals['skipped']} skipped as duplicate or low value")
672
718
  if totals["skipped"]
673
719
  else "",
@@ -693,6 +739,36 @@ def import_memories(ctx: dict) -> dict:
693
739
  SERVER_BULK_MAX = 50
694
740
 
695
741
 
742
+ def _pick_ref(entry: dict, ref_field: str | None) -> str | None:
743
+ """The record's own id, for ``--resume``.
744
+
745
+ Checked in the order an export is likely to carry one. ``metadata.mem0_id``
746
+ is included because the Mem0 migration guide tells people to put it there, so
747
+ a file built by following our own documentation resumes without extra flags.
748
+ Numbers are accepted and stringified; a caller-supplied id is frequently an
749
+ integer and refusing it would be pedantry.
750
+ """
751
+ metadata = entry.get("metadata") if isinstance(entry.get("metadata"), dict) else {}
752
+ if ref_field:
753
+ candidates = [entry.get(ref_field), metadata.get(ref_field)]
754
+ else:
755
+ candidates = [
756
+ entry.get("id"),
757
+ entry.get("_id"),
758
+ entry.get("uuid"),
759
+ metadata.get("mem0_id"),
760
+ ]
761
+ for value in candidates:
762
+ if isinstance(value, str) and value.strip():
763
+ return value.strip()
764
+ # bool is an int subclass and is never a usable identifier.
765
+ if isinstance(value, int) and not isinstance(value, bool):
766
+ return str(value)
767
+ if isinstance(value, float) and value == int(value):
768
+ return str(int(value))
769
+ return None
770
+
771
+
696
772
  def _describe_read_failure(error: OSError) -> str:
697
773
  """Why a file could not be read, worded identically to the Node CLI.
698
774
 
@@ -372,8 +372,8 @@
372
372
  {
373
373
  "name": "import",
374
374
  "summary": "Bulk load memories from a JSON or JSONL file.",
375
- "description": "Each record needs a text field, and may carry its own user and metadata. Validates the whole file before sending anything, and reports per-row errors with line numbers rather than failing on the first bad row.",
376
- "usage": "memorysync import <file> [--user <id>] [--batch-size <n>]",
375
+ "description": "Each record needs a text field, and may carry its own user and metadata. Validates the whole file before sending anything, and reports per-row errors with line numbers rather than failing on the first bad row. Pass --resume to make re-running the same file safe: each record is sent with an identifier the server remembers, so rows that already landed are reported as skipped instead of being stored a second time.",
376
+ "usage": "memorysync import <file> [--user <id>] [--batch-size <n>] [--resume]",
377
377
  "flags": [
378
378
  {
379
379
  "name": "--user",
@@ -386,6 +386,15 @@
386
386
  "value": "n",
387
387
  "description": "Records per request. Default 50."
388
388
  },
389
+ {
390
+ "name": "--resume",
391
+ "description": "Send a per-record identifier so re-running the same file skips rows already stored."
392
+ },
393
+ {
394
+ "name": "--ref-field",
395
+ "value": "name",
396
+ "description": "Field holding each record’s own id, used by --resume. Default: id, then _id, then uuid."
397
+ },
389
398
  {
390
399
  "name": "--continue-on-error",
391
400
  "description": "Keep going after a failed batch."
@@ -396,7 +405,8 @@
396
405
  }
397
406
  ],
398
407
  "examples": [
399
- "memorysync import memories.jsonl --user alice --dry-run"
408
+ "memorysync import memories.jsonl --user alice --dry-run",
409
+ "memorysync import memories.jsonl --user alice --resume"
400
410
  ],
401
411
  "requires_auth": true,
402
412
  "consumes_quota": "add_requests"
@@ -1 +0,0 @@
1
- __version__ = "1.2.1"
File without changes
File without changes