memorysync-cli 1.2.1__tar.gz → 1.4.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.4.0}/PKG-INFO +1 -1
  2. memorysync_cli-1.4.0/src/memorysync_cli/_version.py +1 -0
  3. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/commands/memory.py +198 -4
  4. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/http.py +97 -1
  5. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/main.py +1 -0
  6. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/registry.json +48 -3
  7. memorysync_cli-1.2.1/src/memorysync_cli/_version.py +0 -1
  8. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/.gitignore +0 -0
  9. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/LICENSE +0 -0
  10. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/README.md +0 -0
  11. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/pyproject.toml +0 -0
  12. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/__init__.py +0 -0
  13. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/__main__.py +0 -0
  14. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/args.py +0 -0
  15. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/commands/__init__.py +0 -0
  16. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/commands/admin.py +0 -0
  17. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/commands/init.py +0 -0
  18. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/commands/source.py +0 -0
  19. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/commands/tooling.py +0 -0
  20. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/completions.py +0 -0
  21. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/config.py +0 -0
  22. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/credentials.py +0 -0
  23. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/errors.py +0 -0
  24. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/evaluation.py +0 -0
  25. {memorysync_cli-1.2.1 → memorysync_cli-1.4.0}/src/memorysync_cli/output.py +0 -0
  26. {memorysync_cli-1.2.1 → memorysync_cli-1.4.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.4.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.4.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,44 @@ 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
+
630
+ # --async hands the whole file to the server and returns a job id. The
631
+ # synchronous path below is capped at 50 records per request by the route, so a
632
+ # large file is thousands of round trips held open by one command; this is one
633
+ # upload and a poll.
634
+ if flags.get("async"):
635
+ job = api.create_import(
636
+ filename=Path(source_path).name,
637
+ content=raw.encode("utf-8"),
638
+ end_user_id=user,
639
+ resume=bool(flags.get("resume")),
640
+ continue_on_error=bool(flags.get("continue_on_error")),
641
+ )
642
+ waited = bool(flags.get("wait"))
643
+ finished = _poll_import(api, job["id"]) if waited else job
644
+ return {
645
+ "data": {"file": source_path, "user": user, **finished},
646
+ "text": lambda: _render_import_job(finished, waited=waited),
647
+ }
648
+
602
649
  batch_size = _resolve_batch_size(flags)
603
650
  batches = [records[i : i + batch_size] for i in range(0, len(records), batch_size)]
604
651
  totals = {
@@ -606,23 +653,31 @@ def import_memories(ctx: dict) -> dict:
606
653
  "imported": 0,
607
654
  "memory_ids": [],
608
655
  "skipped": 0,
656
+ # Rows the server had already stored under the same identifier. Counted
657
+ # apart from ``skipped`` because they mean something different to whoever
658
+ # is resuming: nothing was lost, this row is simply already in.
659
+ "already_imported": 0,
609
660
  "failed": 0,
610
661
  "unreported": 0,
611
662
  "failures": [],
612
663
  "batches": len(batches),
613
664
  "batch_size": batch_size,
665
+ "resume": bool(flags.get("resume")),
614
666
  }
615
667
 
668
+ resume = bool(flags.get("resume"))
616
669
  for batch in batches:
617
670
  before = totals["failed"]
618
- items = [
619
- {
671
+ items = []
672
+ for record in batch:
673
+ item = {
620
674
  key: value
621
675
  for key, value in record.items()
622
676
  if key in {"text", "source", "metadata"}
623
677
  }
624
- for record in batch
625
- ]
678
+ if resume and record.get("ref"):
679
+ item["client_ref"] = record["ref"]
680
+ items.append(item)
626
681
  try:
627
682
  response = api.bulk_add_memories({"items": items}) or {}
628
683
  except Exception as error: # noqa: BLE001 — reported, not raised
@@ -653,6 +708,11 @@ def import_memories(ctx: dict) -> dict:
653
708
  if status == "created":
654
709
  totals["imported"] += 1
655
710
  totals["memory_ids"].extend(item.get("memory_ids") or [])
711
+ elif status == "skipped" and item.get("reason") == "already_ingested":
712
+ totals["already_imported"] += 1
713
+ # The ids the first run produced, so a resumed import still ends
714
+ # up knowing every memory its file corresponds to.
715
+ totals["memory_ids"].extend(item.get("memory_ids") or [])
656
716
  elif status == "skipped":
657
717
  totals["skipped"] += 1
658
718
  else:
@@ -668,6 +728,11 @@ def import_memories(ctx: dict) -> dict:
668
728
  line
669
729
  for line in [
670
730
  f"{style.green('Imported')} {totals['imported']} of {totals['total']}",
731
+ style.dim(
732
+ f" {totals['already_imported']} already imported on an earlier run"
733
+ )
734
+ if totals["already_imported"]
735
+ else "",
671
736
  style.dim(f" {totals['skipped']} skipped as duplicate or low value")
672
737
  if totals["skipped"]
673
738
  else "",
@@ -692,6 +757,135 @@ def import_memories(ctx: dict) -> dict:
692
757
  # higher ``--batch-size`` is clamped rather than sent and refused.
693
758
  SERVER_BULK_MAX = 50
694
759
 
760
+ #: Terminal job states. Polling stops at any of these.
761
+ IMPORT_TERMINAL = frozenset({"completed", "failed", "cancelled"})
762
+
763
+
764
+ def _poll_import(api, job_id: str) -> dict:
765
+ """Poll a job until it finishes.
766
+
767
+ Backs off from one second to five. A tight loop on a job that takes an hour is
768
+ thousands of pointless requests, and a fixed long interval makes a ten-second
769
+ job feel broken.
770
+ """
771
+ import time
772
+
773
+ wait_s = 1.0
774
+ while True:
775
+ job = api.get_import(job_id) or {}
776
+ if job.get("status") in IMPORT_TERMINAL:
777
+ return job
778
+ time.sleep(wait_s)
779
+ wait_s = min(5.0, wait_s * 1.5)
780
+
781
+
782
+ def _render_import_job(job: dict, *, waited: bool = False) -> str:
783
+ """One job as a human-readable block.
784
+
785
+ Shared by ``import --async`` and ``import-status``.
786
+ """
787
+ status = job.get("status") or "unknown"
788
+ if status == "completed":
789
+ label = style.green("Completed")
790
+ elif status == "failed":
791
+ label = style.red("Failed")
792
+ elif status == "cancelled":
793
+ label = style.yellow("Cancelled")
794
+ else:
795
+ label = style.dim(status)
796
+
797
+ memories = job.get("memories_created") or 0
798
+ lines = [f"{label} {style.dim(job.get('id') or '')}"]
799
+ if job.get("total_rows"):
800
+ lines.append(
801
+ f"{style.dim('progress')} {job.get('processed_rows')}/{job.get('total_rows')}"
802
+ f" ({job.get('progress_percentage')}%)"
803
+ )
804
+ lines.append(
805
+ f"{style.dim('imported')} {job.get('created') or 0} record(s), "
806
+ f"{memories} memor{'y' if memories == 1 else 'ies'}"
807
+ )
808
+ if job.get("already_imported"):
809
+ lines.append(
810
+ style.dim(f" {job['already_imported']} already imported on an earlier run")
811
+ )
812
+ if job.get("skipped"):
813
+ lines.append(style.dim(f" {job['skipped']} skipped as duplicate or low value"))
814
+ if job.get("failed"):
815
+ lines.append(style.red(f" {job['failed']} failed"))
816
+ for problem in (job.get("invalid_rows") or [])[:5]:
817
+ lines.append(style.dim(f" {problem}"))
818
+ if job.get("error_message"):
819
+ lines.append(style.red(job["error_message"]))
820
+ if not waited and status not in IMPORT_TERMINAL:
821
+ lines.append(style.dim(f"Check again with: memorysync import-status {job.get('id')}"))
822
+ return "\n".join(lines)
823
+
824
+
825
+ def import_status(ctx: dict) -> dict:
826
+ """Poll, list, or cancel a background import."""
827
+ flags, positionals, api = ctx["flags"], ctx["positionals"], ctx["api"]
828
+ job_id = positionals[0] if positionals else None
829
+
830
+ if not job_id:
831
+ response = api.list_imports(flags.get("limit")) or {}
832
+ jobs = response.get("jobs") or []
833
+ return {
834
+ "data": {"jobs": jobs, "count": len(jobs)},
835
+ "text": lambda: (
836
+ style.dim("No imports yet.")
837
+ if not jobs
838
+ else "\n\n".join(_render_import_job(job) for job in jobs)
839
+ ),
840
+ }
841
+
842
+ if flags.get("cancel"):
843
+ job = api.cancel_import(job_id) or {}
844
+ return {
845
+ "data": job,
846
+ "text": lambda: "\n".join(
847
+ [
848
+ style.yellow("Cancellation requested."),
849
+ style.dim("Records already imported stay imported."),
850
+ _render_import_job(job),
851
+ ]
852
+ ),
853
+ }
854
+
855
+ waited = bool(flags.get("wait"))
856
+ job = _poll_import(api, job_id) if waited else (api.get_import(job_id) or {})
857
+ return {"data": job, "text": lambda: _render_import_job(job, waited=waited)}
858
+
859
+
860
+ def _pick_ref(entry: dict, ref_field: str | None) -> str | None:
861
+ """The record's own id, for ``--resume``.
862
+
863
+ Checked in the order an export is likely to carry one. ``metadata.mem0_id``
864
+ is included because the Mem0 migration guide tells people to put it there, so
865
+ a file built by following our own documentation resumes without extra flags.
866
+ Numbers are accepted and stringified; a caller-supplied id is frequently an
867
+ integer and refusing it would be pedantry.
868
+ """
869
+ metadata = entry.get("metadata") if isinstance(entry.get("metadata"), dict) else {}
870
+ if ref_field:
871
+ candidates = [entry.get(ref_field), metadata.get(ref_field)]
872
+ else:
873
+ candidates = [
874
+ entry.get("id"),
875
+ entry.get("_id"),
876
+ entry.get("uuid"),
877
+ metadata.get("mem0_id"),
878
+ ]
879
+ for value in candidates:
880
+ if isinstance(value, str) and value.strip():
881
+ return value.strip()
882
+ # bool is an int subclass and is never a usable identifier.
883
+ if isinstance(value, int) and not isinstance(value, bool):
884
+ return str(value)
885
+ if isinstance(value, float) and value == int(value):
886
+ return str(int(value))
887
+ return None
888
+
695
889
 
696
890
  def _describe_read_failure(error: OSError) -> str:
697
891
  """Why a file could not be read, worded identically to the Node CLI.
@@ -26,6 +26,8 @@ import time
26
26
  import urllib.error
27
27
  import urllib.parse
28
28
  import urllib.request
29
+ import uuid
30
+ from dataclasses import dataclass, field
29
31
  from typing import Any
30
32
 
31
33
  from ._version import __version__
@@ -42,6 +44,50 @@ from .errors import (
42
44
  END_USER_HEADER = "X-End-User-ID"
43
45
 
44
46
 
47
+ @dataclass
48
+ class MultipartBody:
49
+ """One file plus simple text fields, for an upload."""
50
+
51
+ filename: str
52
+ content: bytes
53
+ fields: dict[str, str] = field(default_factory=dict)
54
+
55
+
56
+ def _encode_multipart(part: MultipartBody) -> tuple[bytes, str]:
57
+ """Build a ``multipart/form-data`` body by hand.
58
+
59
+ Written out rather than pulled from a library because this package has zero
60
+ dependencies, and the Node CLI's built-in ``FormData`` produces the same wire
61
+ format, so the two uploads are interchangeable.
62
+
63
+ The boundary is random per request. A fixed one would break the moment a
64
+ payload happened to contain it.
65
+ """
66
+ boundary = f"----MemorySync{uuid.uuid4().hex}"
67
+ lines: list[bytes] = []
68
+
69
+ for name, value in part.fields.items():
70
+ lines.append(f"--{boundary}\r\n".encode())
71
+ lines.append(
72
+ f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode()
73
+ )
74
+ lines.append(f"{value}\r\n".encode())
75
+
76
+ # The filename is quoted and any quote inside it escaped, so a file called
77
+ # `my"file.jsonl` cannot break out of the header.
78
+ safe_name = part.filename.replace('"', '\\"')
79
+ lines.append(f"--{boundary}\r\n".encode())
80
+ lines.append(
81
+ f'Content-Disposition: form-data; name="file"; filename="{safe_name}"\r\n'.encode()
82
+ )
83
+ lines.append(b"Content-Type: application/octet-stream\r\n\r\n")
84
+ lines.append(part.content)
85
+ lines.append(b"\r\n")
86
+ lines.append(f"--{boundary}--\r\n".encode())
87
+
88
+ return b"".join(lines), f"multipart/form-data; boundary={boundary}"
89
+
90
+
45
91
  class ApiClient:
46
92
  """A thin, synchronous client over the MemorySync API."""
47
93
 
@@ -100,6 +146,7 @@ class ApiClient:
100
146
  body: Any = None,
101
147
  query: dict[str, Any] | None = None,
102
148
  timeout: int | None = None,
149
+ multipart: "MultipartBody | None" = None,
103
150
  ) -> Any:
104
151
  url = self.base_url + path
105
152
  if query:
@@ -113,7 +160,10 @@ class ApiClient:
113
160
 
114
161
  payload = None
115
162
  headers = self.headers()
116
- if body is not None:
163
+ if multipart is not None:
164
+ payload, content_type = _encode_multipart(multipart)
165
+ headers["Content-Type"] = content_type
166
+ elif body is not None:
117
167
  payload = json.dumps(body).encode("utf-8")
118
168
  headers["Content-Type"] = "application/json"
119
169
 
@@ -368,6 +418,52 @@ class ApiClient:
368
418
  """
369
419
  return self.request("GET", "/evaluation/usage")
370
420
 
421
+ # ── Asynchronous bulk import ──────────────────────────────────────
422
+
423
+ def create_import(
424
+ self,
425
+ *,
426
+ filename: str,
427
+ content: bytes,
428
+ end_user_id: str | None,
429
+ resume: bool,
430
+ continue_on_error: bool,
431
+ ) -> Any:
432
+ """Upload a payload and get a job id back.
433
+
434
+ Sent as multipart rather than JSON because the payload can be tens of
435
+ megabytes and the server streams it out of storage rather than holding it.
436
+ """
437
+ fields: dict[str, str] = {
438
+ "resume": "true" if resume else "false",
439
+ "continue_on_error": "true" if continue_on_error else "false",
440
+ }
441
+ if end_user_id:
442
+ fields["end_user_id"] = end_user_id
443
+ return self.request(
444
+ "POST",
445
+ "/imports",
446
+ multipart=MultipartBody(
447
+ fields=fields,
448
+ filename=filename,
449
+ content=content,
450
+ ),
451
+ # An upload is not a 30-second request. The server validates the whole
452
+ # payload before answering.
453
+ timeout=300_000,
454
+ )
455
+
456
+ def get_import(self, job_id: str) -> Any:
457
+ return self.request("GET", f"/imports/{urllib.parse.quote(job_id, safe='')}")
458
+
459
+ def list_imports(self, limit: int | None = None) -> Any:
460
+ return self.request("GET", "/imports", query={"limit": limit})
461
+
462
+ def cancel_import(self, job_id: str) -> Any:
463
+ return self.request(
464
+ "POST", f"/imports/{urllib.parse.quote(job_id, safe='')}/cancel"
465
+ )
466
+
371
467
  def bulk_add_memories(self, body: dict) -> Any:
372
468
  """Ingest many records in one request.
373
469
 
@@ -63,6 +63,7 @@ _HANDLERS: dict[str, Handler] = {
63
63
  "update": memory.update,
64
64
  "delete": memory.delete_memories,
65
65
  "import": memory.import_memories,
66
+ "import-status": memory.import_status,
66
67
  "export": memory.export_memories,
67
68
  "quota": admin.quota,
68
69
  "status": admin.status,
@@ -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. Pass --async for a large file: the file is uploaded once and processed in the background, and you get a job id to poll with `memorysync import-status`.",
376
+ "usage": "memorysync import <file> [--user <id>] [--batch-size <n>] [--resume] [--async]",
377
377
  "flags": [
378
378
  {
379
379
  "name": "--user",
@@ -386,6 +386,23 @@
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
+ },
398
+ {
399
+ "name": "--async",
400
+ "description": "Upload the file and return a job id instead of waiting. Use for large files."
401
+ },
402
+ {
403
+ "name": "--wait",
404
+ "description": "With --async, poll until the job finishes."
405
+ },
389
406
  {
390
407
  "name": "--continue-on-error",
391
408
  "description": "Keep going after a failed batch."
@@ -396,11 +413,39 @@
396
413
  }
397
414
  ],
398
415
  "examples": [
399
- "memorysync import memories.jsonl --user alice --dry-run"
416
+ "memorysync import memories.jsonl --user alice --dry-run",
417
+ "memorysync import memories.jsonl --user alice --resume",
418
+ "memorysync import memories.jsonl --user alice --resume --async --wait"
400
419
  ],
401
420
  "requires_auth": true,
402
421
  "consumes_quota": "add_requests"
403
422
  },
423
+ {
424
+ "name": "import-status",
425
+ "summary": "Check a background import, or list recent ones.",
426
+ "description": "With a job id, reports progress and per-outcome counts. Without one, lists recent imports newest first. Pass --wait to poll until the job finishes, and --cancel to ask the worker to stop between batches — records already imported stay imported.",
427
+ "usage": "memorysync import-status [<job-id>] [--wait] [--cancel]",
428
+ "flags": [
429
+ {
430
+ "name": "--wait",
431
+ "description": "Poll until the job reaches a final state."
432
+ },
433
+ {
434
+ "name": "--cancel",
435
+ "description": "Ask the worker to stop between batches."
436
+ },
437
+ {
438
+ "name": "--limit",
439
+ "value": "n",
440
+ "description": "How many recent jobs to list. Default 20."
441
+ }
442
+ ],
443
+ "examples": [
444
+ "memorysync import-status",
445
+ "memorysync import-status 3f9a1c7e-0b2d-4a51-9e88-1c2d3e4f5a6b --wait"
446
+ ],
447
+ "requires_auth": true
448
+ },
404
449
  {
405
450
  "name": "export",
406
451
  "summary": "Write a user's memories to a file or stdout.",
@@ -1 +0,0 @@
1
- __version__ = "1.2.1"
File without changes
File without changes