Compare commits
1494 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6158ac1065 | |||
| 3d3303bf36 | |||
| bd6d170ebb | |||
| a5cdd45651 | |||
| a9455313cc | |||
| 8aa07c2f62 | |||
| d54dad265d | |||
| bac961bc8d | |||
| f21e9fe2b3 | |||
| 844d633960 | |||
| 4f40437d65 | |||
| 477279daeb | |||
| 37d5658171 | |||
| 0414aedce5 | |||
| a2516269cc | |||
| 1c92c466c4 | |||
| bcba0560c4 | |||
| 84ffb36190 | |||
| 70bc9e51d5 | |||
| 0107fd53b8 | |||
| 54a03cc721 | |||
| e95de4ca50 | |||
| a623cfaf76 | |||
| 92491c00c2 | |||
| 9043fbf61e | |||
| c90cbc75a2 | |||
| 58304fc939 | |||
| 397f3414ee | |||
| a734e057cf | |||
| 0fdaa29b4a | |||
| 6d3486ca56 | |||
| ebb9bb2b15 | |||
| 594b4e65d6 | |||
| 1b95c99db4 | |||
| b66cf0f272 | |||
| 72dca1cdf5 | |||
| ece7ff6b84 | |||
| 30ce028a71 | |||
| bd9d27ff50 | |||
| 08b746c9be | |||
| 693e709389 | |||
| 553e275112 | |||
| 43dde3b186 | |||
| cca7551192 | |||
| 5be2630f5b | |||
| 2549a84e5c | |||
| 34ed122ef3 | |||
| db8ac61713 | |||
| 15feaa8ac4 | |||
| 282feaebf2 | |||
| f5dc825d47 | |||
| 32b74e18b7 | |||
| daa4495583 | |||
| cfb5f1776e | |||
| 573e5212a4 | |||
| 8ba225cec8 | |||
| 4b09943092 | |||
| 4e611e8dba | |||
| 5520226b5b | |||
| 00695e3113 | |||
| 7b6790bafb | |||
| 93da5ef8f7 | |||
| c1c5bd62f6 | |||
| 2ec3c4ab20 | |||
| 3fbc1c9aef | |||
| 0b14f75c05 | |||
| fb224083e4 | |||
| 30469aec17 | |||
| 50db9e428d | |||
| fb87349664 | |||
| 8827553576 | |||
| c8e20a9bb5 | |||
| 93a51f4763 | |||
| 86fe275f53 | |||
| e6d6276bb9 | |||
| 9692726db4 | |||
| d8d776636f | |||
| a5a688295e | |||
| 5d40592e42 | |||
| a488e19044 | |||
| 57f944e18a | |||
| fe3f7ae618 | |||
| 4a7e166f9a | |||
| 85768e78e7 | |||
| 7b395f3bf7 | |||
| 4180409b09 | |||
| 649e719ce6 | |||
| 1a53852d93 | |||
| ac9cdd4840 | |||
| cf530c4bec | |||
| 92b958c1cc | |||
| c239d8a483 | |||
| 9d6b79a14e | |||
| e44b46ef2e | |||
| 3882af7450 | |||
| d39ebad09f | |||
| 9d82e2329d | |||
| 789cc9d607 | |||
| e59e3d5f0c | |||
| d926f3697c | |||
| c996b0e7fa | |||
| 78ca85a260 | |||
| 88f696a60a | |||
| 081eca6d8f | |||
| 2434b9d550 | |||
| 3ffea554bc | |||
| 1ad8a59b0c | |||
| a670333d67 | |||
| 4c2db3e68b | |||
| 07f0d4f1e0 | |||
| 3565404eef | |||
| 144627c4ce | |||
| 6984958138 | |||
| b13748c446 | |||
| 4642a1d6e3 | |||
| 686d5e987d | |||
| c55447c1e4 | |||
| ee67602c58 | |||
| 0daa5d7d03 | |||
| cfb3f58e4a | |||
| 66230b3f1f | |||
| 1941cae031 | |||
| fcbb70ab3b | |||
| 33d2bc495d | |||
| c0cae68646 | |||
| 3b2f01796e | |||
| 9cd3d2cca8 | |||
| c53f1f126d | |||
| 0b7615fa87 | |||
| 66d34fab3c | |||
| 868b63af63 | |||
| 6cc1c15320 | |||
| 7a20da59ee | |||
| f89f7c7c81 | |||
| 5723136bed | |||
| b5345f8498 | |||
| 6577ae7616 | |||
| 5e00d5c452 | |||
| 3b152a3e85 | |||
| beca7cc873 | |||
| 30f242dc4c | |||
| 1bfaaf8750 | |||
| c250ccfb5c | |||
| e2b439c42a | |||
| c788d771d3 | |||
| 2acf9571b3 | |||
| 713dba5d0a | |||
| 8ae7a06220 | |||
| f94ea06588 | |||
| 215d8b5a71 | |||
| 82525dbf0f | |||
| 32c1ccba5b | |||
| 9bebcf45f7 | |||
| 93bd4e248c | |||
| af19495f66 | |||
| dbac83218f | |||
| 3618aeff22 | |||
| 25e25aaa2f | |||
| 5d30af9560 | |||
| 47969aaa6b | |||
| 7213e1b1a6 | |||
| 97291aa119 | |||
| b8a5ca1b70 | |||
| 431cba20e9 | |||
| 4482228383 | |||
| ae49edec44 | |||
| 4b7f51d194 | |||
| 41abb571a2 | |||
| e280665578 | |||
| 27a6e7863e | |||
| ae8e03c6b7 | |||
| 376be3b6d4 | |||
| cd2dd7cc54 | |||
| ece654811a | |||
| 13d42a99e9 | |||
| 3225e30859 | |||
| 88fd0e77d0 | |||
| c2698f2b71 | |||
| e87240d4d9 | |||
| 68cf4e118d | |||
| 12624555b4 | |||
| e8004b93db | |||
| 4e414e4015 | |||
| 16455789d4 | |||
| 3ac4e047de | |||
| a2ffca3266 | |||
| 3a9fcdbec2 | |||
| 515f87b6bd | |||
| 5edc0cc99f | |||
| 7fff26f374 | |||
| 1aecfadf45 | |||
| 7e06aeeada | |||
| 2a59c9fd99 | |||
| 669ed184e4 | |||
| 3c2683c1b5 | |||
| f06e2d744d | |||
| f9e30304d7 | |||
| 7e3b727528 | |||
| 13c7f84eec | |||
| 924ac00c52 | |||
| 2a36960f4c | |||
| 2e0f91e70d | |||
| d1b4b304c7 | |||
| 2868bfe749 | |||
| 5431badfd4 | |||
| bda5b726bd | |||
| 16bcc91716 | |||
| ba63ea4528 | |||
| 5332741961 | |||
| d8a6960b4a | |||
| 316dc67a0a | |||
| 65156d5176 | |||
| c5e8216362 | |||
| ecedbc11d9 | |||
| 9aadfa3221 | |||
| cc45561abd | |||
| 7cebaba0a2 | |||
| 5dabf24809 | |||
| ec326f0f92 | |||
| eb780f4880 | |||
| c39d5ada4d | |||
| 06c25eb00b | |||
| 7a09663156 | |||
| 267bcf2931 | |||
| bf9a5703b1 | |||
| 884e740b53 | |||
| 824032a81d | |||
| abdb07c204 | |||
| 7b26df728d | |||
| 30661ab427 | |||
| 7ad5d6f442 | |||
| 305ce7b6b3 | |||
| 4437c3e8a8 | |||
| 54bdbde6e6 | |||
| 2b9558335b | |||
| 2520edb404 | |||
| f05e50d940 | |||
| 401754ca65 | |||
| 73038900f5 | |||
| 6663b738d5 | |||
| 88abb29de9 | |||
| 66e6f58fc6 | |||
| 22c2545d61 | |||
| 410b79c750 | |||
| 08de18f860 | |||
| 46b4b2e9c8 | |||
| a029cc9d43 | |||
| 348f44b632 | |||
| 0c4d0290cb | |||
| b971b61cbb | |||
| ffd1b96916 | |||
| 4ffe1eaa4e | |||
| a172de9c22 | |||
| 7539463f50 | |||
| 577a5a2feb | |||
| d7a34c24dd | |||
| 214d2a1d0d | |||
| f0eb9e091f | |||
| 3cdcb6564c | |||
| 336fbce60a | |||
| 9eb5b9ed29 | |||
| 8230a5dac7 | |||
| 9864584c21 | |||
| 9eea060db9 | |||
| 15218d4a7f | |||
| 69001d7b1f | |||
| 35fe30aabd | |||
| 11a7d8378c | |||
| b4b73deada | |||
| bfe730aa38 | |||
| 82d67430dd | |||
| 8fcf2b0b29 | |||
| 2e5e290434 | |||
| 06ee1b588c | |||
| dc6122ec3d | |||
| df79a43925 | |||
| a6242710df | |||
| 4e1e4c0c5a | |||
| fa5c85f9f6 | |||
| 7c29eb2645 | |||
| 6f079c313f | |||
| e95090e116 | |||
| 861cbb7289 | |||
| 54aa760720 | |||
| 59c3b050bd | |||
| 5b3acf416b | |||
| 63f587c922 | |||
| 21df43c699 | |||
| 0118198143 | |||
| 36537c8326 | |||
| 7482c48692 | |||
| e219961f9d | |||
| 3ffe43f99f | |||
| 72e2c5c24b | |||
| 34c797d285 | |||
| a0d8a02b94 | |||
| 93c720301e | |||
| db15d5c629 | |||
| aa4a944b51 | |||
| ab5e930cf7 | |||
| 0e76de7bd5 | |||
| 5a93643f12 | |||
| a140829395 | |||
| a6810819ca | |||
| a02205e519 | |||
| 69a832dc58 | |||
| 70baa46cb1 | |||
| 3d3e875d21 | |||
| dba7f0458a | |||
| 27e5db5831 | |||
| 2c90eedfff | |||
| a1db0f6362 | |||
| 90a7b1afa0 | |||
| 69a552d8a8 | |||
| 417ebffadd | |||
| 1dc07d3550 | |||
| 65e22e34d9 | |||
| e08f44c5f2 | |||
| 16d989bbcd | |||
| 0f8654bd40 | |||
| 654089fcfc | |||
| 222c6ceea1 | |||
| 84bd6e3b97 | |||
| 5676bebd5f | |||
| f14132db44 | |||
| 903c3635cc | |||
| cc2894aaec | |||
| 97cbff77ef | |||
| e29220efda | |||
| 3b84a234e1 | |||
| 2bca30ebe6 | |||
| 3297ec1a46 | |||
| 61e2a40d55 | |||
| 9f921e27cb | |||
| 568e97d013 | |||
| ac5660e26d | |||
| 76abd5117d | |||
| 978babd3db | |||
| 2b0a457198 | |||
| 6a7277070f | |||
| 4c53930e47 | |||
| 84687fc3d2 | |||
| 3ca939e210 | |||
| 5f5e64b44b | |||
| 5cb6b31690 | |||
| ee8955d08b | |||
| 80c9139c5b | |||
| 7be32641c7 | |||
| 2c18355dd2 | |||
| 61faf71064 | |||
| ac9598a67f | |||
| f98a17c716 | |||
| 639d26e1ac | |||
| f7d7c53001 | |||
| ec1a60bf8d | |||
| 77c71a134a | |||
| 2692e49d50 | |||
| eb2f8a3738 | |||
| 4a30745592 | |||
| 3b1a4c2e68 | |||
| 5227b0a062 | |||
| 8031f0bf8f | |||
| dd3e5363dd | |||
| d5a130b785 | |||
| 8ede1df10a | |||
| 8ba18bf8bc | |||
| de224dd26d | |||
| 9ef644b95e | |||
| 7afbaae7a3 | |||
| 1090784302 | |||
| 394203d1b5 | |||
| 8f5151c344 | |||
| 41cfb3ab1a | |||
| a40314c971 | |||
| ea22e8d9cd | |||
| ce8a285003 | |||
| 4559623501 | |||
| 37c86aa3c0 | |||
| 335a7d7862 | |||
| 64571002f5 | |||
| 9000576173 | |||
| 922471f43b | |||
| b93ce5548b | |||
| ee0202764b | |||
| 8ba032e029 | |||
| fbf3bd640c | |||
| 51ce6f1347 | |||
| 346d89d244 | |||
| 1104b52d99 | |||
| e19b748ad0 | |||
| 517a266d74 | |||
| cbf56477be | |||
| 58cc44ff38 | |||
| 445286a138 | |||
| d68ed11d58 | |||
| 135883935f | |||
| ed5a1e9fc6 | |||
| dc883b0f9e | |||
| 5616844b9c | |||
| 6e1d02c137 | |||
| a199ee4ff8 | |||
| 88ae952483 | |||
| 9df392b26a | |||
| ead210ffe4 | |||
| a015e2ff4a | |||
| d4e98dba38 | |||
| ac72eb5ecc | |||
| 6b5582f474 | |||
| d38e3f1962 | |||
| a0685f3e8c | |||
| d48b1832c7 | |||
| 21d69307dc | |||
| 9e5810dfb7 | |||
| e3f0277cb9 | |||
| e64488b598 | |||
| f5e0fb9e4b | |||
| 77b4b6a2b9 | |||
| 9477184582 | |||
| b27879bfd4 | |||
| f0e8c3f760 | |||
| 5e8d5e4664 | |||
| c8d864c1b6 | |||
| 617aabe5b3 | |||
| 163dafb216 | |||
| cc15a22bf9 | |||
| 64cbe84089 | |||
| c8f9f20dff | |||
| 97fd320bbf | |||
| 748620f29b | |||
| 0b2aa36e98 | |||
| 3d0ece1bcf | |||
| 458d7ab8a3 | |||
| 84af5ad265 | |||
| 6237a6acb9 | |||
| 8c8368781d | |||
| 3b2d0ad0eb | |||
| 9337a873ec | |||
| 76411e2591 | |||
| d523070cbc | |||
| c72bfc3285 | |||
| ee00bd5731 | |||
| f914dca659 | |||
| b64792590e | |||
| a7ac8bf13b | |||
| 4487785cec | |||
| e4c5582808 | |||
| ff399e5528 | |||
| e7013764f7 | |||
| c8ee17b884 | |||
| 346b913ace | |||
| 49ad64708b | |||
| 3a1eff425b | |||
| ebb411b11a | |||
| 8e8f13a48e | |||
| a6a3928091 | |||
| a883b56aa8 | |||
| 246d9e8f69 | |||
| 192db1844c | |||
| 5e895c240a | |||
| b4bd7b48df | |||
| 1bc31b6ae3 | |||
| 7159221d2e | |||
| c89dc72f79 | |||
| 2145ffdb1c | |||
| 23d31a830b | |||
| ab099312d5 | |||
| 8557533b8f | |||
| 72e5a4fcc4 | |||
| b199186163 | |||
| 9e9dcd70e1 | |||
| dbe909c352 | |||
| d65a39c125 | |||
| b60a208c2f | |||
| c2792c6558 | |||
| 2307dc8613 | |||
| 4c748423fc | |||
| 26732771eb | |||
| 148bbf0a5c | |||
| 6e8c6c1cb7 | |||
| f59ef3f2e2 | |||
| 7a0dc7391e | |||
| 7fac6311f4 | |||
| 0e18f54d36 | |||
| 42e60d6724 | |||
| 57a16aeb4b | |||
| 5ea2d56d88 | |||
| 7d1d0ca806 | |||
| 89b67e0834 | |||
| fbe8a2e90f | |||
| 907328aafe | |||
| 0e03d69ed1 | |||
| 0412e62cb1 | |||
| 724c553a2e | |||
| 08e7ae02de | |||
| d0f61d5995 | |||
| 4433666117 | |||
| 37ee3c5eb2 | |||
| c8892bb1fe | |||
| 1a8d175570 | |||
| 0560d87160 | |||
| 9f3fd06334 | |||
| c1ca366edd | |||
| cba3217280 | |||
| 77ea103b5d | |||
| bcc5f42941 | |||
| 3bc5090371 | |||
| de0513fc9f | |||
| ec9b0688d8 | |||
| 0f5612b96d | |||
| 842903b1b1 | |||
| 70d6f9231b | |||
| aae5989e78 | |||
| 6866e56d7a | |||
| 2992c298cb | |||
| 4491e7f9f4 | |||
| c0a930a7d3 | |||
| 05c404d8d3 | |||
| 7484eed4b2 | |||
| 2c496e6376 | |||
| a20b68fcec | |||
| eb7c712aa6 | |||
| 7476c39257 | |||
| 5b0f1a7cf8 | |||
| b336cdf018 | |||
| 60e4e8a662 | |||
| 6d4a78b7c7 | |||
| d39a1d5541 | |||
| 044ad4f131 | |||
| 75482fdb29 | |||
| 6c69599db9 | |||
| b79bfb7c1e | |||
| 5a1083b709 | |||
| ac085db500 | |||
| 2cc253341c | |||
| e3e2da6d45 | |||
| acf7a30d32 | |||
| a4f6751741 | |||
| 6f3fbd087d | |||
| a98842422b | |||
| aaf879322c | |||
| 8139b5887f | |||
| b4b27f099e | |||
| dc877fd3ba | |||
| 2bb0653e67 | |||
| eb24b92227 | |||
| a5ec286fd4 | |||
| 89499aedbe | |||
| 386d8b87ae | |||
| c173ec32d0 | |||
| dd6f6f7a2e | |||
| b6684b96f7 | |||
| 1fa0f0a157 | |||
| 2754f45387 | |||
| ecd4d91046 | |||
| a5a247b161 | |||
| d47cb8d284 | |||
| fa15db089d | |||
| d35065c887 | |||
| cdee6a4ff0 | |||
| 9eb4e77c75 | |||
| c700d790db | |||
| a90b572389 | |||
| 62c330e5b3 | |||
| c70dc7614b | |||
| e0003247c3 | |||
| 888ee766c5 | |||
| c7e91171a0 | |||
| 18c870ec79 | |||
| 3e5f68ee90 | |||
| a0cd4065d9 | |||
| 7c0c4a03c4 | |||
| a8ace18607 | |||
| df43f904d1 | |||
| a5a07d711b | |||
| a40268dd51 | |||
| c59752c6d6 | |||
| aa334fb569 | |||
| 40a5e87022 | |||
| 4dec9ace88 | |||
| e1dc27276b | |||
| 9a12ea7b3c | |||
| e10a509645 | |||
| fe3f10adb8 | |||
| 53c91fb107 | |||
| ecc596b11f | |||
| be37fca1bb | |||
| 849452cc93 | |||
| 1f2df450bb | |||
| 06d86996f2 | |||
| bb14cc42a0 | |||
| fbee8d5c20 | |||
| 855c322da6 | |||
| 240acca3de | |||
| 7ef1378304 | |||
| 9622ac7dff | |||
| 8a280b4a54 | |||
| 1ba9c71f54 | |||
| 5c6fbcaab0 | |||
| b339cab3c1 | |||
| a952df0953 | |||
| 6cebddebbe | |||
| b3d340f59c | |||
| 78e2efc0f2 | |||
| d21970efcc | |||
| 816039036d | |||
| faf1a34f70 | |||
| 6986153c90 | |||
| 8048e0b32f | |||
| af1cfd8139 | |||
| f5c3804f79 | |||
| 443816365a | |||
| 097959d5cc | |||
| bad6e12972 | |||
| d85fcda037 | |||
| dff91154a7 | |||
| c3f3f82a3e | |||
| 70af43c08c | |||
| 1786d907f7 | |||
| 12a268da30 | |||
| 0aefdf5251 | |||
| df72245b6b | |||
| fe872d0776 | |||
| 052d31939d | |||
| 1c44b675d9 | |||
| a1c9a63074 | |||
| 931df14e25 | |||
| 1b0d8bdd2e | |||
| 5c67a5e6bc | |||
| 267e5b13ea | |||
| a22287a3ba | |||
| da59412150 | |||
| c41719ff9a | |||
| f51b39db91 | |||
| 8d61d73d2f | |||
| 10acf78618 | |||
| caeae60dda | |||
| d7b8497b24 | |||
| a96e1d58f7 | |||
| 0d895b28ae | |||
| 84910b40da | |||
| 0e7c34f541 | |||
| 2b58775c17 | |||
| 326f33757b | |||
| c01221d4aa | |||
| 73d9ccac69 | |||
| 5bbd0d9ca9 | |||
| 641be2878d | |||
| eb7f5a774c | |||
| 6e9f8cf218 | |||
| 02a2b59555 | |||
| ec1d7a45d3 | |||
| 725a1aa114 | |||
| d41f19b9ce | |||
| a0fe9ca5b2 | |||
| c81e2efbb0 | |||
| e9f5a882f5 | |||
| 7117a94fbf | |||
| 63e22382de | |||
| 7b3abd06d0 | |||
| e056acb6a8 | |||
| c09dfc3646 | |||
| 6a1ece13dc | |||
| b74cd9162f | |||
| 42c98e5717 | |||
| ad98f542f8 | |||
| 0fce700e65 | |||
| 393a4fd5a6 | |||
| 6d13e83001 | |||
| 1d916c9dd1 | |||
| 07ddd7cb4b | |||
| f412f8bb0d | |||
| 64c3d34deb | |||
| ff6ae478f1 | |||
| cc5686bd0d | |||
| c958664185 | |||
| d43ca06992 | |||
| ba2e479902 | |||
| cd5c3035ab | |||
| 09ac3618a8 | |||
| 3ee4768c14 | |||
| 78912928bc | |||
| 8bd0d2dc24 | |||
| f0bdd2c341 | |||
| bf0c4adc0c | |||
| 2cca50db80 | |||
| b8e4d0980a | |||
| 3613e2f14a | |||
| 541030d69c | |||
| f77a084d1b | |||
| 33abf772ce | |||
| c3c9205ffa | |||
| 9f204dc557 | |||
| 0e98773efb | |||
| 4431bd7d51 | |||
| 0354ab0d6b | |||
| 6dfc193296 | |||
| 9be6850b9d | |||
| d33547a77a | |||
| d77bed2d5d | |||
| 9c89e0ec95 | |||
| ca1ee2d2d7 | |||
| 05f9607282 | |||
| d9236de4ed | |||
| 942727fec6 | |||
| 72396e307d | |||
| 881cf5b5a6 | |||
| 5327d6e50d | |||
| 15a3e20371 | |||
| 19d7beef43 | |||
| 8b789adb15 | |||
| fd065fe9cc | |||
| b5127f7c62 | |||
| 37d9fed690 | |||
| 31861e9acb | |||
| 07462adc9a | |||
| 616313b8b5 | |||
| 44f2490667 | |||
| 3cc794fb98 | |||
| ff6b251c66 | |||
| 55df395fd6 | |||
| 244e60cea6 | |||
| b480c71f0f | |||
| 309c8c18a6 | |||
| f4d8647264 | |||
| f95c4cbbe5 | |||
| 00c7cc432c | |||
| 91abc03880 | |||
| 9100e95175 | |||
| cdb8dcdb9e | |||
| 2a79add7a5 | |||
| 9dfa9b4412 | |||
| 5509066925 | |||
| 3712522b14 | |||
| 93f34e4116 | |||
| 39e5cbfacc | |||
| d30c78c5eb | |||
| 039756abbe | |||
| cb38c2dae7 | |||
| 4dc9f6fad6 | |||
| 1b1f02eb57 | |||
| 1db1105cad | |||
| 91a28c7f04 | |||
| a78e1894b1 | |||
| 83338295d8 | |||
| aff70807c6 | |||
| 648c86b53d | |||
| de0b2de9c2 | |||
| e3f460fba4 | |||
| fe7b336922 | |||
| 1033ab227c | |||
| 5ed15c3bcd | |||
| c1f5a655ba | |||
| 72bb631bb5 | |||
| 2bf9286071 | |||
| 884b597312 | |||
| f1471bcc55 | |||
| 9ae23f9c88 | |||
| cbecbb7b64 | |||
| f8ad0c2b2c | |||
| d0da9a1ae0 | |||
| bc4ab3db77 | |||
| 0eb53b9f27 | |||
| 3cef3ed95e | |||
| 45e5f2af93 | |||
| 32ba13b3ea | |||
| 69a91d5cbb | |||
| 2004427acd | |||
| 2517ccd489 | |||
| 9d0300f774 | |||
| 366d263e0b | |||
| 4321d24284 | |||
| 9cb2a13f3b | |||
| 5ec7889d9a | |||
| b54845bcc9 | |||
| 1ae2747ff8 | |||
| 953a5a4a2d | |||
| 2b49c9eedd | |||
| 9db5f62262 | |||
| a1bd4285db | |||
| e77a10a8da | |||
| e4307ae420 | |||
| 7c89d00079 | |||
| 563eaae5ee | |||
| 6733f78f81 | |||
| c11637bd2f | |||
| ff30cb8ddd | |||
| 2e853c3d22 | |||
| 3cc7013fde | |||
| 8e6a08aa83 | |||
| 8b9a8e5825 | |||
| afc630272d | |||
| e33008e3a4 | |||
| 7b516328a8 | |||
| 6d5889d98f | |||
| ee66e0c954 | |||
| 1aed611539 | |||
| 6c2b131d6e | |||
| 2ffe9922f3 | |||
| 540ada489b | |||
| 65cffa0369 | |||
| 9f937943ba | |||
| 51a68bf7c5 | |||
| 92541d8955 | |||
| 0e0be18ecc | |||
| 66d3f9b93c | |||
| b8f40f728f | |||
| 00a2ea9ff0 | |||
| 9545836469 | |||
| 3acd9e20da | |||
| d48ecd52ef | |||
| 2fbea7705b | |||
| d7a26bd0c3 | |||
| e25dc4b504 | |||
| dab3349990 | |||
| 6db87e8d07 | |||
| faf811ee2d | |||
| ee80a43810 | |||
| 4be426f762 | |||
| ba9c61938b | |||
| 65f826e064 | |||
| b43363cdf3 | |||
| 89e786a88e | |||
| 2d5062bd40 | |||
| b89628322d | |||
| 6e4fb22a7c | |||
| 9c0954133f | |||
| e9a0be66d8 | |||
| 192c33f190 | |||
| 75ca528666 | |||
| e30e4967ae | |||
| d3911b92cf | |||
| 92cfc1c8ef | |||
| 33fcc53e4b | |||
| e761a1e865 | |||
| f2ce92ebcc | |||
| bbb812e0a0 | |||
| 9a302cef30 | |||
| ae729da4d1 | |||
| 655ae794b6 | |||
| 1aef468ebe | |||
| 78baf7495d | |||
| 6cf7ac3e30 | |||
| 07d2f11081 | |||
| c6fbba6a4d | |||
| b701a50b51 | |||
| 5865d79de7 | |||
| 41a42da774 | |||
| 6d7ef3ae45 | |||
| 2c31a930a3 | |||
| c7e2a71cd5 | |||
| 4237b9220b | |||
| cabe29c7c7 | |||
| 80b7202db6 | |||
| 8c6d16a6f0 | |||
| dd1f2989bc | |||
| 540ec1b816 | |||
| 728ef98d6e | |||
| 329d0cc945 | |||
| 0234c85be5 | |||
| eca1e06711 | |||
| 2611343cbe | |||
| 8bde881e2c | |||
| 6fdc63504a | |||
| 23dbce4f59 | |||
| 7c8628eadc | |||
| 20c03eaa92 | |||
| aa7ab9736d | |||
| f7500c925e | |||
| 8b53b1473a | |||
| c611e3e0e7 | |||
| bc4c15962a | |||
| 6e65730b0e | |||
| 8452dd598f | |||
| 2556c5fe88 | |||
| a4340b2336 | |||
| f4dc5f6c71 | |||
| 32ebdaef2f | |||
| 4318663697 | |||
| 5606c3ffb8 | |||
| c1aba35884 | |||
| d9b48191de | |||
| e06f95cea4 | |||
| b131c4bfc4 | |||
| 6acb00731d | |||
| 8143f86be6 | |||
| 8d07469ba7 | |||
| 434b555a29 | |||
| f8071a753b | |||
| d200691e9b | |||
| ecff6315e7 | |||
| ff4510f83d | |||
| 308e79bb68 | |||
| 48176bd194 | |||
| 5cebe9ab52 | |||
| 371848cfbc | |||
| 8e3ed634d1 | |||
| e9bc4cdc95 | |||
| a236aa2315 | |||
| 5660fffa96 | |||
| eba6f77330 | |||
| b5d00e9b6c | |||
| 1be0d70d02 | |||
| edb53209ef | |||
| 7443e58a9d | |||
| 7f25caba47 | |||
| c42934b7fb | |||
| 17887b5959 | |||
| 5d47f4f060 | |||
| 600c9fae63 | |||
| 2d0c8fe94e | |||
| 369d5325f9 | |||
| 29d63306a4 | |||
| 96628d7791 | |||
| 244fd2231d | |||
| 3db028c719 | |||
| c86b1e4d4c | |||
| 5f5b738745 | |||
| acaf47ed54 | |||
| 9734b2db7e | |||
| 3fe31e1c31 | |||
| f4c0f98fde | |||
| 6e781f616c | |||
| dcba83186a | |||
| a6d305f8d0 | |||
| db512950b9 | |||
| d4df9f6dfe | |||
| 0e6b20982a | |||
| 92e1a9b433 | |||
| 3be356a0e9 | |||
| 760cd54ddf | |||
| 1436da18b1 | |||
| cc9acb7493 | |||
| cbee71a63e | |||
| b052a86424 | |||
| 95f5fb3ab4 | |||
| 04d7f2e48c | |||
| be46f4eb40 | |||
| f16a580ef6 | |||
| 095189d39a | |||
| 064d28626b | |||
| cd31c5897a | |||
| 0bb6137877 | |||
| 2c63f4d866 | |||
| 3984b90f62 | |||
| b08b50cbc6 | |||
| 90096d6954 | |||
| 7581974805 | |||
| a8fafb9368 | |||
| 75a4a253f7 | |||
| f9995d144f | |||
| 8d3c8c695d | |||
| 9f27b88843 | |||
| d2f0e23dc8 | |||
| 6e23a6f00e | |||
| a06c9a99ae | |||
| 63fbd2dc2c | |||
| 203943919b | |||
| 04bbad67ac | |||
| e1b527b73f | |||
| 625846caf8 | |||
| c1bd4e19fe | |||
| 8d172d6139 | |||
| a5355f7488 | |||
| f13f3b9283 | |||
| 56351d1f8d | |||
| a9d1383909 | |||
| 80c9c6a577 | |||
| e4e5511642 | |||
| a4b085553a | |||
| e12273c7cb | |||
| ee2b5adfc0 | |||
| 205a03a5f2 | |||
| 7be029a26f | |||
| 0bd177b30c | |||
| 82359774b7 | |||
| 3fa4b80deb | |||
| e96fd5d269 | |||
| 927644d712 | |||
| 7397279872 | |||
| 6851fac327 | |||
| 254524a624 | |||
| 7f0d766c09 | |||
| ac8cf59473 | |||
| 9c4acdcba7 | |||
| a6b9721ede | |||
| a8f3ec25b7 | |||
| 21854c6a24 | |||
| 09bf7ad916 | |||
| 0cc528f3b1 | |||
| cbd845fe41 | |||
| 2e782b0963 | |||
| 4c31c65649 | |||
| c90f87e657 | |||
| c63c0aca9d | |||
| d4dbed9dbd | |||
| e9188a51fe | |||
| d893033dcf | |||
| 78a2ef41d7 | |||
| 542153ad4f | |||
| 49802137fa | |||
| 0091b31762 | |||
| 339a990510 | |||
| 4c2a903618 | |||
| 8851969169 | |||
| 3a5e5851dd | |||
| 716824860b | |||
| 64c80e3fbc | |||
| 9032e68917 | |||
| e7da9eb00c | |||
| 6b99457381 | |||
| be4188c6b4 | |||
| 4d08c16bd8 | |||
| 763f804277 | |||
| 400b1f4eac | |||
| 6bc86d7d2c | |||
| df68a6b397 | |||
| b739cbbc2e | |||
| f645e50c5c | |||
| 2eff703e72 | |||
| dd06333732 | |||
| 847e1cc986 | |||
| af29ecc93f | |||
| 52eaddbd8a | |||
| 06d1757038 | |||
| 9900b4f5e4 | |||
| 25ef5dda53 | |||
| 2e1d257f36 | |||
| 4b34751bda | |||
| ba9c2e68f9 | |||
| 86d3e36ace | |||
| 7284317cef | |||
| 1508a5a418 | |||
| 9b55b717e0 | |||
| 8c087fcabc | |||
| 4b8e32830a | |||
| 62ca0ddbe2 | |||
| bcb41f85c9 | |||
| 751f5d5a19 | |||
| 5ab09ffd5a | |||
| 13374a12e9 | |||
| a02597ed59 | |||
| 8a56f0ed4a | |||
| fd7fab4e08 | |||
| e909e3e76c | |||
| 1ebe5b643d | |||
| c0b9a10224 | |||
| 802231c105 | |||
| 0d5085454b | |||
| 6d535951df | |||
| eaf295756e | |||
| 11894c64b3 | |||
| b9e22beecb | |||
| 6f051036d9 | |||
| 3731965537 | |||
| 549e5e3ce8 | |||
| 48bbfcbb2c | |||
| 4cc91d7505 | |||
| 77b0912808 | |||
| 6a00643bfa | |||
| 2e74667cc6 | |||
| d2b653ab10 | |||
| 2c94e6b817 | |||
| a6ac4a6698 | |||
| e7cc8b9552 | |||
| f6290a0e48 | |||
| 6668be3d5b | |||
| cf12148bc7 | |||
| d928ea4a2b | |||
| efd45c0c4d | |||
| 4896d5c66f | |||
| 61a24f011a | |||
| 8d9eb225a8 | |||
| 605558da9d | |||
| dca74a1ec0 | |||
| fb3eef6cf5 | |||
| 10d3209e5a | |||
| aace88d2e7 | |||
| 228eaa16d5 | |||
| f3416aa46a | |||
| eb32fb912d | |||
| 8c4ee7569f | |||
| fbf1d8c372 | |||
| c5d298eec8 | |||
| 078aa66b90 | |||
| d4ffed9822 | |||
| ff7761aaf8 | |||
| f058cda152 | |||
| b6f9054567 | |||
| 5667bc1eab | |||
| 4661d55913 | |||
| 9c52d72dc1 | |||
| 2cd9f94ea6 | |||
| 2b262a65b2 | |||
| bd5ce7c6d2 | |||
| 20c3aee636 | |||
| 721d765921 | |||
| 84eb666618 | |||
| 3f2d5bee34 | |||
| 9341d9f597 | |||
| aacc7c25d3 | |||
| b59fbb0bd2 | |||
| ae7b1a666e | |||
| bf57d253a5 | |||
| c689f94c52 | |||
| ab862d0d40 | |||
| 29178a4c72 | |||
| d107b639b3 | |||
| c09c4926a7 | |||
| 395af18d88 | |||
| ecefb793fc | |||
| c6b9035956 | |||
| 3513a9def6 | |||
| 0d45c61aa3 | |||
| f324462cc3 | |||
| 52bd8fca5c | |||
| c45f14e77d | |||
| 0dbfcbe6d9 | |||
| 23279d4248 | |||
| 68c7355f47 | |||
| aaf8e6e7ff | |||
| 699741c760 | |||
| 2d3dda3a4c | |||
| 61dd5a5ea4 | |||
| 41be228e5c | |||
| 0491854298 | |||
| 44ee48e924 | |||
| 5525c4e6fe | |||
| 3914f4d6ac | |||
| 8511eca03b | |||
| 56ceecb4e3 | |||
| db5cb1986a | |||
| 6102aa76bb | |||
| fc88cae628 | |||
| 8c3c9e1520 | |||
| 55c54beeab | |||
| 0a78cb9f7a | |||
| 3502344e89 | |||
| 30edf49aaf | |||
| 5b9be679a8 | |||
| 8e2f7f2bfb | |||
| dc5a26fe95 | |||
| d66654bf67 | |||
| 959f4bb059 | |||
| f9634b4bf3 | |||
| 47a8e677e9 | |||
| f40a2e7603 | |||
| ac7b7aa20a | |||
| 5eeeb4e38c | |||
| db835cdcb8 | |||
| e3aca7026b | |||
| 3bd49b57cc | |||
| 5b9b65c395 | |||
| bbddb98aca | |||
| b081e43b8d | |||
| 58f29d8781 | |||
| f01e8a083e | |||
| 7170edd13f | |||
| bf0cf2d9c4 | |||
| 51c4f2aae8 | |||
| a972d2fb07 | |||
| 6a54d27286 | |||
| d32ae1a0b1 | |||
| 965f7a3735 | |||
| 136b5545ec | |||
| 8099d60e0e | |||
| da2fd1a51a | |||
| 18d069d10c | |||
| 851b665c11 | |||
| c674625e88 | |||
| 23e2ed2163 | |||
| 0b1ca090f5 | |||
| bf3ad37369 | |||
| f21ca9b765 | |||
| d113037a4f | |||
| b2f683f3cc | |||
| 83eb800fb3 | |||
| af8454811c | |||
| 8dbfe28bbb | |||
| 5d53b0c2ca | |||
| 65056311a6 | |||
| d03ba0fc8a | |||
| 9804f078d0 | |||
| c886764b62 | |||
| 2262fadd5b | |||
| 462aaebd6c | |||
| 077d0c47f9 | |||
| ad233034ef | |||
| 9d0932971d | |||
| 822a8acedb | |||
| 28bc4fe05b | |||
| df5b7109f5 | |||
| 4bbbc904b6 | |||
| fee3c27af3 | |||
| deeb4f2250 | |||
| a80796b5ff | |||
| a279ed0694 | |||
| 06d6fe7d76 | |||
| 6057cf5202 | |||
| a94bd11a76 | |||
| e8004537c1 | |||
| c8a47b2f98 | |||
| c545dcf412 | |||
| b80925e857 | |||
| 3fb4f2655b | |||
| 324e17b226 | |||
| b3d6e645b7 | |||
| cb2f86551b | |||
| 4f5a40a84f | |||
| ea86dc1576 | |||
| 7de35b4a68 | |||
| 2d66c23116 | |||
| 515fb86497 | |||
| 8ea12ca24b | |||
| 448a21f617 | |||
| a7f5fb59c3 | |||
| c64e0824da | |||
| 9b7a882d57 | |||
| e3a3a48973 | |||
| 6a9f5341b5 | |||
| e31ca239a0 | |||
| 0e0d0b8fc7 | |||
| 8476ce5d8e | |||
| 047a85711e | |||
| 32ea60a5fc | |||
| 1e39a22c52 | |||
| daa65e7b02 | |||
| eb7a7e09eb | |||
| c0232a7d97 | |||
| a8ba7abb7d | |||
| e35786e567 | |||
| 214a1ddca5 | |||
| 10cbee943c | |||
| aba5bb052d | |||
| a461091ba5 | |||
| 64218db7bd | |||
| 2180b83a8b | |||
| f19dfe70d7 | |||
| 31ef9135e7 | |||
| 5cea47947c | |||
| 883ffd7de0 | |||
| e66f277324 | |||
| 01cfad62b1 | |||
| 6cc4a31e91 | |||
| 629bb5bb63 | |||
| b245309242 | |||
| 464a188662 | |||
| 4aae2b5cca | |||
| 442f6d72d0 | |||
| d1c5c70b4c | |||
| e570888437 | |||
| 38965ab6bf | |||
| 7a2fd70184 | |||
| 388c70789a | |||
| d5b3eda16c | |||
| 8c8f4120f4 | |||
| e190445492 | |||
| 2dcbcdf7a5 | |||
| 0ccb1124bd | |||
| 874c2e96ca | |||
| 4a643a8449 | |||
| 37cacb27ec | |||
| a14a4405db | |||
| 7ba46ec5ec | |||
| 296327793c | |||
| 4af6288adb | |||
| de7ee38e45 | |||
| 9f6ec325fb | |||
| b10ec8c34a | |||
| b6cfd960d1 | |||
| 5aa7bedabe | |||
| 04b4807145 | |||
| 5837991e5c | |||
| 81b4431c9b | |||
| 504a87d799 | |||
| 024089d33e | |||
| 1c46fddce1 | |||
| 784b607613 | |||
| 44aa16a0f8 | |||
| 3eff82082e | |||
| d2f6fce52e | |||
| 419dc6598c | |||
| 51092b0b64 | |||
| 918823d805 | |||
| e287fb9a89 | |||
| ab4f872502 | |||
| 8be831f7ed | |||
| 67c775276f | |||
| 12a4934112 | |||
| b386e24f5d | |||
| 58b6887bf5 | |||
| 2269194662 | |||
| 1290c1bb2e | |||
| c197a5fe93 | |||
| 563a130141 | |||
| 80945df4ca | |||
| 45ae1f0313 | |||
| e585d3c1cc | |||
| abd4ec64eb | |||
| 47afe52296 | |||
| f2ddc573f6 | |||
| 6f42a95aab | |||
| ac6b53ed0a | |||
| c39436ae74 | |||
| 607c689cb0 | |||
| 914feb65a0 | |||
| ab3c9f889d | |||
| bb2efb8b8b | |||
| 9148cce8fc | |||
| cbb2b2991d | |||
| fd1d5e0e2b | |||
| 04b0297ae4 | |||
| ef706ad976 | |||
| d9f09c1819 | |||
| 0773b37197 | |||
| c8a5c6f0e9 | |||
| c7b9498693 | |||
| c27ab0585c | |||
| e913c96926 | |||
| 0c9c5fe9c2 | |||
| 51fd7db205 | |||
| e9136c1aa0 | |||
| a546a9f56a | |||
| 40c9abe484 | |||
| c411dc294e | |||
| fb5a3bfd95 | |||
| 7441f1462d | |||
| c9240e7ca6 | |||
| 1e7618dfa4 | |||
| 4e5d34103f | |||
| da435bc025 | |||
| 2a43aa6902 | |||
| b620f8fae3 | |||
| 03f787d5cb | |||
| 19637804b3 | |||
| 80f145fceb | |||
| 34477d4936 | |||
| 4ec51f2dd6 | |||
| f842a92e25 | |||
| 83e8c97295 | |||
| 1a5d0d236a | |||
| ebbf90f4aa | |||
| 4f119692f1 | |||
| bbe56107fb | |||
| bd654e7aac | |||
| 33500a7ce2 | |||
| 4880557d51 | |||
| ea09b5f7f0 | |||
| 5258fd91ea | |||
| b305d674de | |||
| 7c24601d0f | |||
| 50c0285cb2 | |||
| 0a78198bb5 | |||
| edaeb78ccf | |||
| f80be2d2ea | |||
| 8700165b42 | |||
| 18fb92f1f8 | |||
| 14fc6bbadd | |||
| 5070a1d83e | |||
| 8a9088ea9d | |||
| 48b24f6f12 | |||
| f6ddd5ffc5 | |||
| b43a116b3c | |||
| 50512a5f03 | |||
| e3e107b31d | |||
| 21a04541ea | |||
| cdd5d8ac76 | |||
| 11094f504e | |||
| 5acaae5f56 | |||
| 4547d870af | |||
| dc0d8e0932 | |||
| c558eae9ce | |||
| abb9af66a6 | |||
| 4800e0344c | |||
| 439b425c61 | |||
| 2855f1635b | |||
| 08b67b4a78 | |||
| 1bddd46ed2 | |||
| 6ecdadfd97 | |||
| 4119040005 | |||
| 873eef6ef8 | |||
| 445fed4d3f | |||
| 52fd3e0dd4 | |||
| 8fd0e1f3b0 | |||
| 11fc4a8451 | |||
| e22293294e | |||
| 73e53aaff1 | |||
| 6fa946557f | |||
| fb0852f585 | |||
| 4070fc1bf0 | |||
| 00c1fa1ec7 | |||
| 04e77ef34e | |||
| 827d63d115 | |||
| e0d0f6e94c | |||
| fd07513004 | |||
| b0e436d9c4 | |||
| a4bfd9cfc6 | |||
| 8ca01918e5 | |||
| a5b2381458 | |||
| 26c771503b | |||
| 622ed4a7c9 | |||
| 940f0128d5 | |||
| 1354747ca8 | |||
| 9544c69c55 | |||
| 9ba445e623 | |||
| ebc5e25f98 | |||
| 78301ee63d | |||
| 797dea1dca | |||
| a0ff764f0a | |||
| a795798156 | |||
| 1a66f961f4 | |||
| 6fb2048af0 | |||
| ba9f186fc5 | |||
| 6c32d287b5 | |||
| 536f85b78a | |||
| f8619870ad | |||
| d00a2085d5 | |||
| 85ec61335a | |||
| 9b48a12c27 | |||
| c181ccbe42 | |||
| 8520033d44 | |||
| ebdce87fde | |||
| f2122ed696 | |||
| 3616eaadb4 | |||
| ef69c91b60 | |||
| 117824b32c | |||
| f77f5b996e | |||
| a4d32aec24 | |||
| 9111495fae | |||
| ee1e3f0957 | |||
| 4dc5c7348f | |||
| 4428768eaa | |||
| 11f4ce8fb6 | |||
| 6078738d34 | |||
| faacfeb891 | |||
| 8d7e8b6fb9 | |||
| 7e1d2ffdd7 | |||
| 91044ec591 | |||
| c77a75dfb5 | |||
| 6518c0c06b | |||
| 09cdaff9a2 | |||
| 56bf33ab7f | |||
| 752f638cfc | |||
| 92dd7edb57 | |||
| b4bb4cf053 | |||
| f0400e928a | |||
| aa5ad625af | |||
| f8f69eab03 | |||
| 2b2263acaa | |||
| 5e2e7fb639 | |||
| 6c12bc9044 | |||
| 9a11683003 | |||
| 38b4e06963 | |||
| 0766a44ccf | |||
| 036bf3a161 | |||
| 41bd258b93 | |||
| 38e212c721 | |||
| 2f285ea00a | |||
| d38120c839 | |||
| d94aee812b | |||
| 68d650ec40 | |||
| 769d926f5a | |||
| 9478bab04e | |||
| 7ad4af250f | |||
| 9fa368b114 | |||
| 4afef04f26 | |||
| 8fe2c3effc | |||
| fa78c972be | |||
| 0e66261644 | |||
| 819650a254 | |||
| 34c41c87dc | |||
| 2985b667b0 | |||
| 31bb0e7f0f | |||
| 8f28264aec | |||
| ec4fb11aa5 | |||
| b210723de1 | |||
| 433f99dd78 | |||
| e75c05112e | |||
| d2a5b50ff8 | |||
| 120690afd4 | |||
| 344dbeee42 | |||
| 3fe3b0320a | |||
| 75896b647f | |||
| 446d0975aa | |||
| b7d365119c | |||
| 2d9fbd4e49 | |||
| 22e14b5e65 | |||
| 1a654beea4 | |||
| f50f8a444a | |||
| 3cc3a0058d | |||
| ae473b5e3c | |||
| efb7e31565 | |||
| 069d265338 | |||
| 751a3a4bd1 | |||
| cb0499407e |
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"owner": {
|
||||
"name": "Mem0",
|
||||
"email": "support@mem0.ai"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Official Mem0 plugins for Claude"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||
"version": "0.1.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"owner": {
|
||||
"name": "Mem0",
|
||||
"email": "support@mem0.ai"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Official Mem0 plugins for Cursor"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||
"version": "0.1.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
OPENAI_API_KEY="your-openai-api-key"
|
||||
@@ -1,41 +1,55 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us reproduce and fix the bug
|
||||
name: Bug Report
|
||||
description: Report a bug in mem0
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
#### Before submitting a bug, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/embedchain/embedchain/issues?q=is%3Aissue+sort%3Acreated-desc+).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 🐛 Describe the bug
|
||||
description: |
|
||||
Please provide a clear and concise description of what the bug is.
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which part of mem0 is affected?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- REST API
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
If relevant, add a minimal example so that we can reproduce the error by running the code. It is very important for the snippet to be as succinct (minimal) as possible, so please take time to trim down any irrelevant code to help us debug efficiently. We are going to copy-paste your code and we expect to get the same result as you did: avoid any external data, and include the relevant imports, etc. For example:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
value: |
|
||||
### Summary
|
||||
|
||||
```python
|
||||
# All necessary imports at the beginning
|
||||
import embedchain as ec
|
||||
# Your code goes here
|
||||
A clear summary of the bug.
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
```
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
Please also paste or describe the results you observe instead of the expected results. If you observe an error, please paste the error message including the **full** traceback of the exception. It may be relevant to wrap error messages in ```` ```triple quotes blocks``` ````.
|
||||
placeholder: |
|
||||
A clear and concise description of what the bug is.
|
||||
m = Memory()
|
||||
# Your code here...
|
||||
```
|
||||
|
||||
```python
|
||||
Sample code to reproduce the problem
|
||||
```
|
||||
### Expected Behavior
|
||||
|
||||
```
|
||||
The error message you got, with the full traceback.
|
||||
````
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for contributing 🎉!
|
||||
What you expected to happen.
|
||||
|
||||
### Actual Behavior
|
||||
|
||||
What actually happened. Paste the full error traceback if applicable.
|
||||
|
||||
### Environment
|
||||
|
||||
- mem0 version:
|
||||
- Python/Node version:
|
||||
- OS:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 1-on-1 Session
|
||||
url: https://cal.com/taranjeetio/ec
|
||||
about: Speak directly with Taranjeet, the founder, to discuss issues, share feedback, or explore improvements for Embedchain
|
||||
- name: Discord
|
||||
- name: Discord Community
|
||||
url: https://discord.gg/6PzXDgEjG5
|
||||
about: General community discussions
|
||||
about: Ask questions and discuss with the community
|
||||
- name: Documentation
|
||||
url: https://docs.mem0.ai
|
||||
about: Read the official mem0 documentation
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
name: Documentation
|
||||
description: Report an issue related to the Embedchain docs.
|
||||
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
|
||||
name: Documentation Issue
|
||||
description: Report an issue or suggest an improvement to the mem0 docs
|
||||
labels: ["documentation"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Issue with current documentation:"
|
||||
description: >
|
||||
Please make sure to leave a reference to the document/code you're
|
||||
referring to.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
value: |
|
||||
### Page
|
||||
|
||||
Link to the docs page: https://docs.mem0.ai/...
|
||||
|
||||
### What's Wrong or Missing
|
||||
|
||||
Describe what's incorrect, unclear, or missing.
|
||||
|
||||
### Suggested Fix
|
||||
|
||||
How should the docs be improved?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
name: 🚀 Feature request
|
||||
description: Submit a proposal/request for a new Embedchain feature
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement for mem0
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-request
|
||||
attributes:
|
||||
label: 🚀 The feature
|
||||
description: >
|
||||
A clear and concise description of the feature proposal
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation, pitch
|
||||
description: >
|
||||
Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., *"I'm working on X and would like Y to be possible"*. If this is related to another GitHub issue, please link here too.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for contributing 🎉!
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which part of mem0 does this relate to?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- REST API
|
||||
- Benchmarks / Evals
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
value: |
|
||||
### Use Case
|
||||
|
||||
What problem are you trying to solve?
|
||||
|
||||
### Proposed Solution
|
||||
|
||||
How should this work? Include API examples or pseudocode if helpful.
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
Any workarounds you've tried or other approaches considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
## Linked Issue
|
||||
|
||||
Closes #<!-- issue number -->
|
||||
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
<!-- What does this PR do? Why is it needed? -->
|
||||
|
||||
Fixes # (issue)
|
||||
## Type of Change
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Refactor (does not change functionality, e.g. code style improvements, linting)
|
||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||
- [ ] New feature (non-breaking change that adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Refactor (no functional changes)
|
||||
- [ ] Documentation update
|
||||
|
||||
## How Has This Been Tested?
|
||||
## Breaking Changes
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
||||
<!-- If this is a breaking change, describe what breaks and the migration path. Delete this section if not applicable. -->
|
||||
|
||||
Please delete options that are not relevant.
|
||||
N/A
|
||||
|
||||
- [ ] Unit Test
|
||||
- [ ] Test Script (please provide)
|
||||
## Test Coverage
|
||||
|
||||
## Checklist:
|
||||
- [ ] I added/updated unit tests
|
||||
- [ ] I added/updated integration tests
|
||||
- [ ] I tested manually (describe below)
|
||||
- [ ] No tests needed (explain why)
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||
- [ ] I have checked my code and corrected any misspellings
|
||||
<!-- Describe how you tested this, or link to CI results. -->
|
||||
|
||||
## Maintainer Checklist
|
||||
## Checklist
|
||||
|
||||
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
|
||||
- [ ] Made sure Checks passed
|
||||
- [ ] My code follows the project's style guidelines
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have added tests that prove my fix/feature works
|
||||
- [ ] New and existing tests pass locally
|
||||
- [ ] I have updated documentation if needed
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Maps dropdown selections to GitHub labels
|
||||
# Used by the advanced-issue-labeler GitHub Action
|
||||
|
||||
component:
|
||||
- label: "sdk-python"
|
||||
matcher: "Core / Python SDK"
|
||||
- label: "sdk-typescript"
|
||||
matcher: "TypeScript SDK"
|
||||
- label: "vector-store"
|
||||
matcher: "Vector Store"
|
||||
- label: "graph-memory"
|
||||
matcher: "Graph Memory"
|
||||
- label: "ollama"
|
||||
matcher: "Ollama"
|
||||
- label: "openclaw"
|
||||
matcher: "OpenClaw"
|
||||
- label: "rest-api"
|
||||
matcher: "REST API"
|
||||
+18
-13
@@ -2,14 +2,14 @@ name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published] # This will trigger the workflow when you create a new release
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
if: startsWith(github.event.release.tag_name, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# IMPORTANT: this permission is mandatory for trusted publishing
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -19,22 +19,27 @@ jobs:
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Poetry
|
||||
- name: Install Hatch
|
||||
run: |
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
pip install hatch
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry install
|
||||
run: |
|
||||
hatch env create
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: poetry build
|
||||
run: |
|
||||
hatch build --clean
|
||||
|
||||
- name: Publish distribution 📦 to Test PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
# TODO: Needs to setup mem0 repo on Test PyPI
|
||||
# - name: Publish distribution 📦 to Test PyPI
|
||||
# uses: pypa/gh-action-pypi-publish@release/v1
|
||||
# with:
|
||||
# repository_url: https://test.pypi.org/legacy/
|
||||
# packages_dir: dist/
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages_dir: dist/
|
||||
|
||||
+123
-20
@@ -4,47 +4,150 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'embedchain/**'
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- 'examples/**'
|
||||
- 'embedchain/**'
|
||||
- '.github/workflows/**'
|
||||
- 'pyproject.toml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'embedchain/**'
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- 'examples/**'
|
||||
- 'embedchain/**'
|
||||
- 'pyproject.toml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
changelog_check:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Require CHANGELOG entry when Python SDK version changes
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
extract_version() {
|
||||
python3 -c "import sys, re; m = re.search(r'^\s*version\s*=\s*\"([^\"]+)\"', sys.stdin.read(), re.M); print(m.group(1) if m else '')"
|
||||
}
|
||||
|
||||
base_version=$(git show "$BASE_SHA:pyproject.toml" 2>/dev/null | extract_version || echo "")
|
||||
head_version=$(extract_version < pyproject.toml)
|
||||
|
||||
echo "Base version: ${base_version:-<unknown>}"
|
||||
echo "Head version: $head_version"
|
||||
|
||||
if [ -z "$base_version" ] || [ "$base_version" = "$head_version" ]; then
|
||||
echo "pyproject.toml version unchanged — no CHANGELOG entry required."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Detected version bump ${base_version} -> ${head_version}. Checking docs/changelog/sdk.mdx…"
|
||||
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- docs/changelog/sdk.mdx | grep -q .; then
|
||||
echo "Changelog update present in docs/changelog/sdk.mdx ✅"
|
||||
else
|
||||
echo "::error file=pyproject.toml::pyproject.toml version changed from ${base_version} to ${head_version} but docs/changelog/sdk.mdx was not updated in this PR. Add a new <Update> entry under the Python tab for v${head_version}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
mem0_changed: ${{ steps.filter.outputs.mem0 }}
|
||||
embedchain_changed: ${{ steps.filter.outputs.embedchain }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
mem0:
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- '.github/workflows/**'
|
||||
- 'pyproject.toml'
|
||||
embedchain:
|
||||
- 'embedchain/**'
|
||||
|
||||
build_mem0:
|
||||
needs: check_changes
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11"]
|
||||
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
version: 1.4.2
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
- name: Clean up disk space
|
||||
run: |
|
||||
df -h
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
||||
sudo docker image prune --all --force
|
||||
sudo docker builder prune -a
|
||||
df -h
|
||||
- name: Install Hatch
|
||||
run: pip install hatch
|
||||
- name: Load cached venv
|
||||
id: cached-poetry-dependencies
|
||||
uses: actions/cache@v2
|
||||
id: cached-hatch-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
|
||||
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
|
||||
- name: Install GEOS Libraries
|
||||
run: sudo apt-get update && sudo apt-get install -y libgeos-dev
|
||||
- name: Install dependencies
|
||||
run: poetry install --all-extras
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -e ".[test,graph,vector_stores,llms,extras]"
|
||||
pip install ruff
|
||||
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Run Linting
|
||||
run: make lint
|
||||
- name: Run tests and generate coverage report
|
||||
run: make coverage
|
||||
run: make test
|
||||
|
||||
build_embedchain:
|
||||
needs: check_changes
|
||||
if: needs.check_changes.outputs.embedchain_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Hatch
|
||||
run: pip install hatch
|
||||
- name: Load cached venv
|
||||
id: cached-hatch-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-embedchain-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
|
||||
- name: Install dependencies
|
||||
run: cd embedchain && make install_all
|
||||
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Run Formatting
|
||||
run: |
|
||||
mkdir -p embedchain/.ruff_cache && chmod -R 777 embedchain/.ruff_cache
|
||||
cd embedchain && hatch run format
|
||||
- name: Lint with ruff
|
||||
run: cd embedchain && make lint
|
||||
- name: Run tests and generate coverage report
|
||||
run: cd embedchain && make coverage
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Publish @mem0/cli 📦 to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/cli 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'cli-node-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: cli/node
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
@@ -0,0 +1,100 @@
|
||||
name: CLI Node CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: cli/node
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
working-directory: cli/node
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Type check
|
||||
working-directory: cli/node
|
||||
run: pnpm run typecheck
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: cli/node
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
working-directory: cli/node
|
||||
run: pnpm run test
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: cli/node
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
working-directory: cli/node
|
||||
run: pnpm run build
|
||||
|
||||
- name: Verify dist output
|
||||
run: |
|
||||
test -f cli/node/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Publish mem0-cli 🐍 distributions 📦 to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish mem0-cli 📦 to PyPI
|
||||
if: startsWith(github.event.release.tag_name, 'cli-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: cli/python
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Hatch
|
||||
run: pip install hatch
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: hatch build --clean
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: cli/python/dist/
|
||||
@@ -0,0 +1,79 @@
|
||||
name: CLI Python CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dev dependencies
|
||||
working-directory: cli/python
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
- name: Lint with ruff
|
||||
working-directory: cli/python
|
||||
run: ruff check .
|
||||
|
||||
- name: Check formatting
|
||||
working-directory: cli/python
|
||||
run: ruff format --check .
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dev dependencies
|
||||
working-directory: cli/python
|
||||
run: pip install -e ".[dev]"
|
||||
|
||||
- name: Run tests
|
||||
working-directory: cli/python
|
||||
run: pytest
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Hatch
|
||||
run: pip install hatch
|
||||
|
||||
- name: Build
|
||||
working-directory: cli/python
|
||||
run: hatch build --clean
|
||||
|
||||
- name: Verify dist output
|
||||
run: |
|
||||
ls cli/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
|
||||
ls cli/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
|
||||
@@ -0,0 +1,45 @@
|
||||
name: docs - llms.txt check
|
||||
|
||||
# Blocks PRs that introduce new .mdx pages without a matching entry in
|
||||
# docs/llms.txt, or that link to pages that no longer exist. Contributors
|
||||
# must update docs/llms.txt in the same PR. Run locally with:
|
||||
# python scripts/check-llms-txt-coverage.py # read-only
|
||||
# python scripts/check-llms-txt-coverage.py --write # scaffold placeholders
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**/*.mdx'
|
||||
- 'docs/llms.txt'
|
||||
- 'scripts/check-llms-txt-coverage.py'
|
||||
- 'scripts/llms-txt-ignore.txt'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-llms-txt:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Verify docs/llms.txt coverage
|
||||
run: |
|
||||
if ! python3 scripts/check-llms-txt-coverage.py; then
|
||||
echo ""
|
||||
echo "::error title=llms.txt out of sync::docs/llms.txt does not match docs/**/*.mdx."
|
||||
echo ""
|
||||
echo "To fix:"
|
||||
echo " 1. Run locally: python scripts/check-llms-txt-coverage.py --write"
|
||||
echo " This appends placeholder entries under '## Unclassified - needs triage'."
|
||||
echo " 2. For each placeholder:"
|
||||
echo " - replace [TODO: Platform|OSS|Both] with the correct scope tag"
|
||||
echo " - rewrite the description as 'Use when ...'"
|
||||
echo " - move the entry into the appropriate section"
|
||||
echo " - delete the '## Unclassified - needs triage' heading once empty"
|
||||
echo " 3. Resolve any stale URLs listed above by updating or removing the link."
|
||||
echo " 4. Commit the updated docs/llms.txt to this PR."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Auto-label issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: issue-parser
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
with:
|
||||
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: feature-parser
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/feature_request.yml
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
with:
|
||||
issue-form: ${{ steps.feature-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Publish @mem0/openclaw-mem0 📦 to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/openclaw-mem0 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'openclaw-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openclaw
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
@@ -0,0 +1,100 @@
|
||||
name: openclaw checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd openclaw && pnpm exec tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: cd openclaw && pnpm exec vitest run --coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: matrix.node-version == 20
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: openclaw
|
||||
directory: openclaw/coverage
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd openclaw && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Close stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
# Issue settings
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: 'stale'
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not
|
||||
had any activity in 90 days. It will be closed in 14 days if no
|
||||
further activity occurs. If this is still relevant, please leave a
|
||||
comment or remove the `stale` label.
|
||||
close-issue-message: >
|
||||
This issue has been closed due to inactivity. If this is still
|
||||
relevant, feel free to reopen it or create a new issue.
|
||||
|
||||
# PR settings — mark stale but never auto-close
|
||||
days-before-pr-stale: 90
|
||||
days-before-pr-close: -1
|
||||
stale-pr-label: 'stale'
|
||||
stale-pr-message: >
|
||||
This pull request has been automatically marked as stale because it
|
||||
has not had any activity in 90 days. Please update your branch and
|
||||
address any review comments, or it may be closed in the future.
|
||||
|
||||
# Exempt these labels from stale processing
|
||||
exempt-issue-labels: 'P0-critical,P1-high,good first issue,security'
|
||||
exempt-pr-labels: 'P0-critical,P1-high'
|
||||
|
||||
# Remove stale label when there is new activity
|
||||
remove-stale-when-updated: true
|
||||
|
||||
# Process up to 100 issues per run to stay within API limits
|
||||
operations-per-run: 100
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Publish mem0ai 📦 to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish mem0ai 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'ts-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mem0-ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: mem0-ts/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
@@ -0,0 +1,146 @@
|
||||
name: TypeScript SDK CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'mem0-ts/**'
|
||||
- '.github/workflows/ts-sdk-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'mem0-ts/**'
|
||||
|
||||
jobs:
|
||||
check_changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ts_sdk_changed: ${{ steps.filter.outputs.ts_sdk }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
ts_sdk:
|
||||
- 'mem0-ts/**'
|
||||
|
||||
changelog_check:
|
||||
needs: check_changes
|
||||
if: github.event_name == 'pull_request' && needs.check_changes.outputs.ts_sdk_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Require CHANGELOG entry when SDK version changes
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
base_version=$(git show "$BASE_SHA:mem0-ts/package.json" 2>/dev/null | jq -r .version || echo "")
|
||||
head_version=$(jq -r .version mem0-ts/package.json)
|
||||
|
||||
echo "Base version: ${base_version:-<unknown>}"
|
||||
echo "Head version: $head_version"
|
||||
|
||||
if [ -z "$base_version" ] || [ "$base_version" = "$head_version" ]; then
|
||||
echo "mem0-ts/package.json version unchanged — no CHANGELOG entry required."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Detected version bump ${base_version} -> ${head_version}. Checking docs/changelog/sdk.mdx…"
|
||||
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- docs/changelog/sdk.mdx | grep -q .; then
|
||||
echo "Changelog update present in docs/changelog/sdk.mdx ✅"
|
||||
else
|
||||
echo "::error file=mem0-ts/package.json::mem0-ts/package.json version changed from ${base_version} to ${head_version} but docs/changelog/sdk.mdx was not updated in this PR. Add a new <Update> entry under the TypeScript tab for v${head_version}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_ts_sdk:
|
||||
needs: check_changes
|
||||
if: needs.check_changes.outputs.ts_sdk_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: mem0-ts/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: mem0-ts
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
working-directory: mem0-ts
|
||||
run: npx prettier --check .
|
||||
|
||||
- name: Build
|
||||
working-directory: mem0-ts
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run unit tests
|
||||
working-directory: mem0-ts
|
||||
run: pnpm run test:unit
|
||||
|
||||
- name: Verify package exports
|
||||
working-directory: mem0-ts
|
||||
run: |
|
||||
node -e "const m = require('./dist/index.js'); console.log('Client exports:', Object.keys(m).length)"
|
||||
node -e "const m = require('./dist/oss/index.js'); console.log('OSS exports:', Object.keys(m).length)"
|
||||
|
||||
- name: Upload coverage
|
||||
if: matrix.node-version == 20
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report
|
||||
path: mem0-ts/coverage/
|
||||
|
||||
integration_ts_sdk:
|
||||
needs: build_ts_sdk
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
node-version: [20, 22]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: mem0-ts/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: mem0-ts
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
working-directory: mem0-ts
|
||||
run: pnpm run build
|
||||
|
||||
- name: Run integration tests (with cleanup)
|
||||
working-directory: mem0-ts
|
||||
env:
|
||||
MEM0_API_KEY: ${{ secrets.MEM0_API_KEY }}
|
||||
run: pnpm run test:integration
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Publish @mem0/vercel-ai-provider 📦 to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: vercel-ai-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
+15
-3
@@ -2,6 +2,11 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
**/node_modules/
|
||||
|
||||
# Self-hosted server local runtime state
|
||||
server/history/
|
||||
server/.env
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
@@ -14,8 +19,8 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
/lib/
|
||||
/lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
@@ -103,7 +108,7 @@ ipython_config.py
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended not to include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
@@ -165,6 +170,7 @@ cython_debug/
|
||||
# Database
|
||||
db
|
||||
test-db
|
||||
!embedchain/embedchain/core/db/
|
||||
|
||||
.vscode
|
||||
.idea/
|
||||
@@ -178,3 +184,9 @@ notebooks/*.yaml
|
||||
|
||||
# cache db
|
||||
*.db
|
||||
|
||||
# local directories for testing
|
||||
eval/
|
||||
qdrant_storage/
|
||||
.crossnote
|
||||
testing.ipynb
|
||||
|
||||
+12
-16
@@ -1,20 +1,16 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.220'
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: ruff
|
||||
# Respect `exclude` and `extend-exclude` settings.
|
||||
args: ["--force-exclude"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pytest-check
|
||||
name: pytest-check
|
||||
entry: poetry run pytest
|
||||
- id: ruff
|
||||
name: Ruff
|
||||
entry: ruff check
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
types: [python]
|
||||
args: [--fix]
|
||||
|
||||
- id: isort
|
||||
name: isort
|
||||
entry: isort
|
||||
language: system
|
||||
types: [python]
|
||||
args: ["--profile", "black"]
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides context for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, etc.) working with the Mem0 repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Mem0** ("mem-zero") is an intelligent memory layer for AI agents and assistants. It provides persistent, personalized memory via both a hosted platform API and self-hosted open-source SDKs.
|
||||
|
||||
- **Repository**: https://github.com/mem0ai/mem0
|
||||
- **Documentation**: https://docs.mem0.ai
|
||||
- **License**: Apache-2.0
|
||||
|
||||
## Repository Structure
|
||||
|
||||
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
|
||||
|
||||
### Key Directories
|
||||
|
||||
| Directory | Description |
|
||||
|-----------|-------------|
|
||||
| `mem0/` | Core Python SDK (`mem0ai` on PyPI) — memory, LLMs, embeddings, vector stores, graphs, rerankers |
|
||||
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
|
||||
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
|
||||
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
|
||||
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||
| `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
|
||||
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
|
||||
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
|
||||
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/` |
|
||||
| `docs/` | Documentation site (Mintlify) |
|
||||
| `tests/` | Python SDK tests (pytest) |
|
||||
| `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
|
||||
| `examples/` | Sample projects — demo apps, Chrome extension, multi-agent patterns |
|
||||
| `cookbooks/` | Jupyter notebooks — customer support chatbot, AutoGen integration |
|
||||
| `embedchain/` | Legacy Embedchain RAG framework (maintained separately, Poetry-based) |
|
||||
| `pr-reviews/` | Pull request review materials |
|
||||
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
|
||||
|
||||
### Core Package Dependencies
|
||||
|
||||
```
|
||||
mem0 (Python SDK) mem0-ts (TypeScript SDK)
|
||||
├── mem0/memory/ ├── src/client/ (MemoryClient — hosted)
|
||||
├── mem0/llms/ └── src/oss/ (Memory — self-hosted)
|
||||
├── mem0/embeddings/ ├── src/llms/
|
||||
├── mem0/vector_stores/ ├── src/embeddings/
|
||||
├── mem0/graphs/ ├── src/vector_stores/
|
||||
└── mem0/reranker/ └── src/graphs/
|
||||
|
||||
cli/python/ ──▶ mem0ai (optional, for OSS mode)
|
||||
cli/node/ ──▶ mem0ai (npm, for API calls)
|
||||
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||
openclaw/ ──▶ mem0ai (npm)
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Python**: 3.9+ (3.10+ for CLI)
|
||||
- **Node.js**: v18+ (v20 or v22 recommended)
|
||||
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
|
||||
- **Hatch**: Python build/environment tool (`pip install hatch`)
|
||||
- **Docker**: Required for `server/` and `openmemory/` development
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# Python SDK
|
||||
hatch shell dev_py_3_11 # creates environment with all deps
|
||||
pre-commit install # install git hooks
|
||||
|
||||
# TypeScript packages
|
||||
cd mem0-ts && pnpm install # TS SDK
|
||||
cd cli/node && pnpm install # Node CLI
|
||||
cd vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||
cd openclaw && pnpm install # OpenClaw plugin
|
||||
```
|
||||
|
||||
## Build, Lint, and Test Commands
|
||||
|
||||
### Python SDK (`mem0/`)
|
||||
|
||||
```bash
|
||||
# Environment setup (uses Hatch)
|
||||
hatch shell dev_py_3_11 # or dev_py_3_9, dev_py_3_10, dev_py_3_12
|
||||
|
||||
# Linting and formatting
|
||||
make lint # ruff check
|
||||
make format # ruff format
|
||||
make sort # isort mem0/
|
||||
|
||||
# Tests
|
||||
make test # pytest tests/
|
||||
make test-py-3.9 # test specific Python version (3.9–3.12)
|
||||
|
||||
# Build and publish
|
||||
make build # hatch build
|
||||
make publish # hatch publish
|
||||
```
|
||||
|
||||
- **Python:** 3.9, 3.10, 3.11, 3.12
|
||||
- **Linter/formatter:** Ruff (line length **120**)
|
||||
- **Import sorting:** isort (`profile = "black"`)
|
||||
- **Test framework:** pytest (with pytest-mock, pytest-asyncio)
|
||||
- **Pre-commit hooks:** ruff + isort — run `pre-commit install` before committing
|
||||
|
||||
### TypeScript SDK (`mem0-ts/`)
|
||||
|
||||
```bash
|
||||
cd mem0-ts
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run test # jest (all tests)
|
||||
pnpm run test:unit # jest --coverage (unit tests only)
|
||||
pnpm run test:integration # jest (integration tests, needs MEM0_API_KEY)
|
||||
pnpm run test:ci # jest --coverage --ci (CI mode)
|
||||
pnpm run test:watch # jest watch mode
|
||||
```
|
||||
|
||||
- **Node:** 20, 22 (CI-tested)
|
||||
- **Build:** tsup (CJS + ESM)
|
||||
- **Test:** jest
|
||||
- **Formatter:** prettier
|
||||
|
||||
### Python CLI (`cli/python/`)
|
||||
|
||||
```bash
|
||||
cd cli/python
|
||||
pip install -e ".[dev]" # dev install with ruff + pytest
|
||||
ruff check . # lint
|
||||
ruff format . # format
|
||||
pytest # test
|
||||
hatch build # build
|
||||
```
|
||||
|
||||
- **Python:** 3.10+ (not 3.9)
|
||||
- **Linter/formatter:** Ruff (line length **100** — different from root SDK)
|
||||
- **Ruff rules:** E, F, I, W, UP, B, SIM, RUF (ignores E501, B008 for Typer patterns, SIM108)
|
||||
- **Framework:** Typer + Rich + httpx
|
||||
- **Entry point:** `mem0 = "mem0_cli.app:main"`
|
||||
- **Source layout:** `src/mem0_cli/`
|
||||
- **Optional dependency:** `mem0ai` (for OSS mode, via `[oss]` extra)
|
||||
|
||||
### Node CLI (`cli/node/`)
|
||||
|
||||
```bash
|
||||
cd cli/node
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run lint # biome check src/
|
||||
pnpm run lint:fix # biome check --write src/
|
||||
pnpm run typecheck # tsc --noEmit
|
||||
pnpm run test # vitest run
|
||||
pnpm run test:watch # vitest (watch mode)
|
||||
pnpm run dev # tsx src/index.ts (development)
|
||||
```
|
||||
|
||||
- **Node:** 18+ required
|
||||
- **Build:** tsup (ESM)
|
||||
- **Linter:** Biome (not ESLint, not Ruff)
|
||||
- **Test:** vitest (not jest)
|
||||
- **Framework:** Commander + Chalk + ora + cli-table3
|
||||
|
||||
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
|
||||
|
||||
```bash
|
||||
cd vercel-ai-sdk
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run lint # eslint
|
||||
pnpm run type-check # tsc --noEmit
|
||||
pnpm run prettier-check # prettier --check
|
||||
pnpm run test # jest
|
||||
pnpm run test:edge # vitest (edge runtime)
|
||||
pnpm run test:node # vitest (node runtime)
|
||||
```
|
||||
|
||||
- **Build:** tsup (CJS + ESM)
|
||||
- **Lint:** ESLint + Prettier
|
||||
- **Test:** jest + vitest (edge/node configs)
|
||||
|
||||
### OpenClaw Plugin (`openclaw/`)
|
||||
|
||||
```bash
|
||||
cd openclaw
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run test # vitest run
|
||||
```
|
||||
|
||||
- **Build:** tsup (ESM)
|
||||
- **Test:** vitest (with Codecov in CI)
|
||||
- **Plugin manifest:** `openclaw.plugin.json`
|
||||
|
||||
### Server (`server/`)
|
||||
|
||||
```bash
|
||||
# Docker production build
|
||||
cd server
|
||||
make build # docker build -t mem0-api-server .
|
||||
make run_local # docker run -p 8000:8000 with .env
|
||||
|
||||
# Docker Compose development (FastAPI + PostgreSQL/pgvector + Neo4j)
|
||||
cd server
|
||||
docker-compose up # starts all 3 services
|
||||
# mem0 API: localhost:8888
|
||||
# PostgreSQL: localhost:8432
|
||||
# Neo4j HTTP: localhost:8474, Bolt: localhost:8687
|
||||
```
|
||||
|
||||
- **Framework:** FastAPI with uvicorn (auto-reload in dev)
|
||||
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
|
||||
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
|
||||
|
||||
### OpenMemory (`openmemory/`)
|
||||
|
||||
```bash
|
||||
# Full stack via Docker Compose
|
||||
cd openmemory
|
||||
docker-compose up
|
||||
# Qdrant: localhost:6333
|
||||
# API (MCP): localhost:8765
|
||||
# UI: localhost:3000
|
||||
|
||||
# Individual development
|
||||
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
|
||||
cd openmemory/ui && npm run dev # Next.js frontend
|
||||
|
||||
# Tests
|
||||
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
|
||||
```
|
||||
|
||||
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
|
||||
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
|
||||
- **Vector store:** Qdrant
|
||||
|
||||
### Documentation (`docs/`)
|
||||
|
||||
```bash
|
||||
make docs # or: cd docs && mintlify dev
|
||||
```
|
||||
|
||||
- **Framework:** Mintlify
|
||||
- **API spec:** `docs/openapi.json`
|
||||
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
|
||||
|
||||
### Evaluation (`evaluation/`)
|
||||
|
||||
```bash
|
||||
cd evaluation
|
||||
make run-mem0-add # Run mem0 add experiments
|
||||
make run-mem0-search # Run mem0 search experiments
|
||||
make run-mem0-plus-add # With graph memory
|
||||
make run-mem0-plus-search # With graph memory
|
||||
make run-rag # RAG baseline
|
||||
make run-full-context # Full context baseline
|
||||
make run-langmem # LangMem comparison
|
||||
make run-openai # OpenAI comparison
|
||||
```
|
||||
|
||||
## Core APIs
|
||||
|
||||
### Python
|
||||
|
||||
| Function / Class | Purpose | Import |
|
||||
|-----------------|---------|--------|
|
||||
| `Memory` | Self-hosted memory (sync) | `from mem0 import Memory` |
|
||||
| `AsyncMemory` | Self-hosted memory (async) | `from mem0 import AsyncMemory` |
|
||||
| `MemoryClient` | Hosted platform client (sync) | `from mem0 import MemoryClient` |
|
||||
| `AsyncMemoryClient` | Hosted platform client (async) | `from mem0 import AsyncMemoryClient` |
|
||||
|
||||
**Key `Memory` / `MemoryClient` methods:**
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `add(messages, *, user_id, agent_id, run_id, metadata)` | Store a new memory |
|
||||
| `search(query, *, user_id, agent_id, run_id, limit, filters)` | Search memories |
|
||||
| `get(memory_id)` | Retrieve a single memory by ID |
|
||||
| `get_all(*, user_id, agent_id, run_id, limit)` | List all memories |
|
||||
| `update(memory_id, data)` | Update a memory |
|
||||
| `delete(memory_id)` | Delete a memory |
|
||||
| `delete_all(*, user_id, agent_id, run_id)` | Delete all memories |
|
||||
| `history(memory_id)` | Get change history for a memory |
|
||||
|
||||
### TypeScript
|
||||
|
||||
| Export | Purpose | Import |
|
||||
|--------|---------|--------|
|
||||
| `MemoryClient` | Hosted platform client | `import { MemoryClient } from 'mem0ai'` |
|
||||
| `Memory` | Self-hosted OSS memory | `import { Memory } from 'mem0ai/oss'` |
|
||||
|
||||
## Import Patterns
|
||||
|
||||
### Python
|
||||
|
||||
| What | Import |
|
||||
|------|--------|
|
||||
| Core memory classes | `from mem0 import Memory, AsyncMemory` |
|
||||
| Platform client | `from mem0 import MemoryClient, AsyncMemoryClient` |
|
||||
| Configuration | `from mem0.configs.base import MemoryConfig` |
|
||||
| LLM providers | `from mem0.llms.<provider> import <ProviderLLM>` |
|
||||
| Embedding providers | `from mem0.embeddings.<provider> import <ProviderEmbedding>` |
|
||||
| Vector store providers | `from mem0.vector_stores.<provider> import <ProviderVectorStore>` |
|
||||
|
||||
### TypeScript
|
||||
|
||||
| What | Import |
|
||||
|------|--------|
|
||||
| Hosted client | `import { MemoryClient } from 'mem0ai'` |
|
||||
| OSS memory | `import { Memory } from 'mem0ai/oss'` |
|
||||
| Specific providers (OSS) | `import { OpenAIEmbedding } from 'mem0ai/oss'` |
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### File Naming Conventions
|
||||
|
||||
- **Python source files:** `snake_case.py` (e.g., `azure_openai.py`, `cohere_reranker.py`)
|
||||
- **Python test files:** `test_<module>.py` (e.g., `test_memory.py`, `test_main.py`)
|
||||
- **TypeScript source files:** `snake_case.ts` (e.g., `azure_ai_search.ts`)
|
||||
- **TypeScript test files:** `<module>.test.ts` (e.g., `memory.test.ts`)
|
||||
- **Config/manifest files:** `kebab-case` (e.g., `openclaw.plugin.json`, `jest.config.js`)
|
||||
|
||||
### Python Conventions
|
||||
|
||||
- **Provider pattern:** All providers (LLMs, embeddings, vector stores, graphs, rerankers) inherit from a `base.py` abstract class in their directory. Config classes live in `configs.py`.
|
||||
- **Pydantic v2** for all data models and configuration.
|
||||
- **Ruff** is the single linting and formatting tool — no black, no flake8.
|
||||
- Root SDK: line length **120**
|
||||
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
|
||||
- **isort** with `profile = "black"` for import sorting.
|
||||
- Ruff excludes `embedchain/` and `openmemory/` from root config.
|
||||
|
||||
### TypeScript Conventions
|
||||
|
||||
- **Build:** tsup across all packages.
|
||||
- **Package manager:** pnpm everywhere (no npm, no yarn).
|
||||
- **TypeScript strict mode** across all packages.
|
||||
- **Linting varies by package:**
|
||||
|
||||
| Package | Linter | Formatter | Test Framework |
|
||||
|---------|--------|-----------|---------------|
|
||||
| `mem0-ts/` | — | Prettier | jest |
|
||||
| `cli/node/` | Biome | Biome | vitest |
|
||||
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||
| `openclaw/` | — | — | vitest |
|
||||
|
||||
### Type Checking
|
||||
|
||||
Always run type checking after modifying TypeScript code:
|
||||
|
||||
```bash
|
||||
cd <package> && pnpm run typecheck # or: tsc --noEmit
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Provider Pattern
|
||||
|
||||
The SDK uses a consistent plugin architecture across 5 categories. Each category has a `base.py` abstract class and concrete provider implementations:
|
||||
|
||||
| Category | Count | Examples |
|
||||
|----------|-------|---------|
|
||||
| **LLMs** | 24 | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Gemini, Groq, Ollama, Together, DeepSeek, vLLM, LiteLLM, LM Studio, xAI |
|
||||
| **Vector Stores** | 30 | Qdrant, Pinecone, Chroma, Weaviate, Milvus, MongoDB, Redis, Elasticsearch, pgvector, Supabase, Faiss, S3 Vectors |
|
||||
| **Embeddings** | 15 | OpenAI, Azure OpenAI, Gemini, HuggingFace, FastEmbed, Together, AWS Bedrock, Ollama, Vertex AI |
|
||||
| **Graph Stores** | 4 | Neo4j, Memgraph, Kuzu, Apache AGE |
|
||||
| **Rerankers** | 5 | Cohere, HuggingFace, LLM-based, Sentence Transformer, Zero Entropy |
|
||||
|
||||
### Two Usage Modes
|
||||
|
||||
Self-hosted `Memory` / `AsyncMemory` classes and hosted-platform `MemoryClient` — both in Python and TypeScript.
|
||||
|
||||
### Graph Memory
|
||||
|
||||
Optional layer on top of vector memory for relationship-aware retrieval. Configured via the `graph` section of `MemoryConfig`.
|
||||
|
||||
### MCP Integration
|
||||
|
||||
Model Context Protocol support in multiple places:
|
||||
|
||||
- **Remote:** MCP server at `mcp.mem0.ai`
|
||||
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
|
||||
- **Plugin:** MCP tools in `mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
|
||||
|
||||
### Plugin & Skills System
|
||||
|
||||
- `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
|
||||
- `skills/` contains structured skill definitions for AI agents, split into two categories:
|
||||
- **Reference skills** (always-on SDK knowledge): `mem0` (Python + TS SDKs, framework integrations), `mem0-cli` (terminal workflows), `mem0-vercel-ai-sdk` (Vercel AI provider).
|
||||
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch. The two are loosely coupled via `.mem0-integration/` artifacts.
|
||||
|
||||
### Adding a New Provider
|
||||
|
||||
To add a new LLM, embedding, vector store, or reranker provider:
|
||||
|
||||
1. Create `mem0/<category>/<provider_name>.py`
|
||||
2. Inherit from the abstract base class in `mem0/<category>/base.py`
|
||||
3. Add configuration to `mem0/<category>/configs.py` (if the category uses one)
|
||||
4. Register the provider in `mem0/<category>/__init__.py`
|
||||
5. Add tests in `tests/<category>/<provider_name>/`
|
||||
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
|
||||
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
|
||||
|
||||
## CI/CD
|
||||
|
||||
### CI Workflows (automated testing)
|
||||
|
||||
| Workflow | File | Triggers | Tests |
|
||||
|----------|------|----------|-------|
|
||||
| Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
|
||||
| Embedchain | `ci.yml` (shared) | PRs on `embedchain/` | Ruff + pytest + coverage on Python 3.9–3.12 |
|
||||
|
||||
### CD Workflows (automated publishing)
|
||||
|
||||
| Workflow | File | Tag Prefix | Target |
|
||||
|----------|------|------------|--------|
|
||||
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
|
||||
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
|
||||
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
|
||||
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
|
||||
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
|
||||
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
|
||||
|
||||
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
|
||||
|
||||
### Utility Workflows
|
||||
|
||||
| Workflow | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
|
||||
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
|
||||
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync. Fix locally with `python scripts/check-llms-txt-coverage.py --write`. |
|
||||
|
||||
## Task Completion Guidelines
|
||||
|
||||
These guidelines outline typical artifacts for different task types. Use judgment to adapt based on scope and context.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
1. **Unit tests**: Add tests that would fail without the fix (regression tests)
|
||||
2. **Implementation**: Fix the bug
|
||||
3. **Manual verification**: Run the relevant test suite to confirm the fix
|
||||
4. **Lint**: Run the appropriate linter for the package you modified
|
||||
|
||||
### New Features
|
||||
|
||||
1. **Implementation**: Build the feature following existing patterns
|
||||
2. **Unit tests**: Comprehensive test coverage for new functionality
|
||||
3. **Documentation**: Update relevant docs in `docs/` for public APIs
|
||||
4. **Examples**: Add usage examples if the feature introduces new user-facing behavior
|
||||
5. **llms.txt**: Any new `.mdx` page under `docs/` must be linked in `docs/llms.txt` with a scope tag (`[Platform]` / `[OSS]` / `[Both]`) and a `Use when ...` description. The `docs-llms-txt-check.yml` workflow runs on every PR that touches docs and **fails the check** if the index is out of sync. To fix: run `python scripts/check-llms-txt-coverage.py --write` locally to scaffold placeholders under `## Unclassified - needs triage`, then replace the `[TODO: ...]` tags, rewrite descriptions as `Use when ...`, move entries into the right section, and delete the triage heading when empty.
|
||||
|
||||
### New Provider (LLM / Embedding / Vector Store / Reranker)
|
||||
|
||||
1. **Implementation**: Follow the "Adding a New Provider" steps above
|
||||
2. **Tests**: Add unit tests matching the pattern of existing providers
|
||||
3. **Configuration**: Add to the appropriate `configs.py` and `__init__.py`
|
||||
4. **Dependencies**: Add to the correct optional group in `pyproject.toml`
|
||||
5. **Documentation**: Add an integration guide in `docs/integrations/`
|
||||
|
||||
### Refactoring / Internal Changes
|
||||
|
||||
- Unit tests for any changed behavior
|
||||
- No documentation needed for internal-only changes
|
||||
- Ensure all existing tests still pass
|
||||
|
||||
### When to Deviate
|
||||
|
||||
These are guidelines, not rigid rules. Adjust based on:
|
||||
|
||||
- **Scope**: Trivial fixes (typos, comments) may not need tests
|
||||
- **Visibility**: Internal changes may not need documentation
|
||||
- **Context**: Some changes span multiple categories — use judgment
|
||||
|
||||
When uncertain about expected artifacts, ask for clarification.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Fork and clone the repository.
|
||||
2. Create a feature branch from `main` (e.g., `feature/my-new-feature`).
|
||||
3. Make your changes — add tests, docs, and examples as appropriate.
|
||||
4. Run linting and tests for every package you modified (see commands above).
|
||||
5. Run `pre-commit install` on first setup — hooks run ruff + isort automatically.
|
||||
6. Commit with a clear message following [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`, `docs:`, `refactor:`).
|
||||
7. Push and open a Pull Request against `main`.
|
||||
|
||||
### Pull Request Requirements
|
||||
|
||||
Every PR must follow the repo's PR template (`.github/PULL_REQUEST_TEMPLATE.md`):
|
||||
|
||||
1. **Linked Issue** — Reference the issue with `Closes #<number>`. If no issue exists, create one first or explain why in the description.
|
||||
2. **Description** — Explain what the PR does and why it's needed.
|
||||
3. **Type of Change** — Check the appropriate box:
|
||||
- Bug fix / New feature / Breaking change / Refactor / Documentation update
|
||||
4. **Breaking Changes** — If applicable, describe what breaks and the migration path.
|
||||
5. **Test Coverage** — Check what applies:
|
||||
- Added/updated unit tests
|
||||
- Added/updated integration tests
|
||||
- Tested manually (describe how)
|
||||
- No tests needed (explain why)
|
||||
6. **Checklist** — All must be checked before merge:
|
||||
- [ ] Code follows the project's style guidelines
|
||||
- [ ] Self-review performed
|
||||
- [ ] Tests added that prove the fix/feature works
|
||||
- [ ] New and existing tests pass locally
|
||||
- [ ] Documentation updated if needed
|
||||
|
||||
### PR Description Template
|
||||
|
||||
```markdown
|
||||
## Linked Issue
|
||||
|
||||
Closes #<!-- issue number -->
|
||||
|
||||
## Description
|
||||
|
||||
<!-- What does this PR do? Why is it needed? -->
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||
- [ ] New feature (non-breaking change that adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Refactor (no functional changes)
|
||||
- [ ] Documentation update
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
N/A
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- [ ] I added/updated unit tests
|
||||
- [ ] I added/updated integration tests
|
||||
- [ ] I tested manually (describe below)
|
||||
- [ ] No tests needed (explain why)
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] My code follows the project's style guidelines
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have added tests that prove my fix/feature works
|
||||
- [ ] New and existing tests pass locally
|
||||
- [ ] I have updated documentation if needed
|
||||
```
|
||||
|
||||
### General Rules
|
||||
|
||||
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
|
||||
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
|
||||
- For `server/` and `openmemory/` work, use Docker Compose for local development.
|
||||
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
|
||||
|
||||
### Contributing Guides
|
||||
|
||||
| Task | Guide |
|
||||
|------|-------|
|
||||
| Code contributions | `docs/contributing/development.mdx` |
|
||||
| Documentation contributions | `docs/contributing/documentation.mdx` |
|
||||
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
|
||||
| Bug reports | `.github/ISSUE_TEMPLATE/bug_report.yml` |
|
||||
| Feature requests | `.github/ISSUE_TEMPLATE/feature_request.yml` |
|
||||
| Documentation issues | `.github/ISSUE_TEMPLATE/documentation_issue.yml` |
|
||||
|
||||
## Do NOT
|
||||
|
||||
- Modify CI/CD workflows without explicit approval.
|
||||
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
|
||||
- Commit `.env` files, API keys, or credentials.
|
||||
- Modify `embedchain/` unless specifically working on that package — it has its own build system (Poetry).
|
||||
- Skip pre-commit hooks.
|
||||
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
|
||||
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
|
||||
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
|
||||
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
|
||||
- Change public APIs without updating documentation in `docs/`.
|
||||
+54
-37
@@ -1,4 +1,4 @@
|
||||
# Contributing to embedchain
|
||||
# Contributing to mem0
|
||||
|
||||
Let us make contribution easy, collaborative and fun.
|
||||
|
||||
@@ -10,27 +10,26 @@ To make a contribution, follow these steps:
|
||||
2. Do the changes on your fork with dedicated feature branch `feature/f1`
|
||||
3. If you modified the code (new feature or bug-fix), please add tests for it
|
||||
4. Include proper documentation / docstring and examples to run the feature
|
||||
5. Check the linting
|
||||
6. Ensure that all tests pass
|
||||
7. Submit a pull request
|
||||
5. Ensure that all tests pass
|
||||
6. Submit a pull request
|
||||
|
||||
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
|
||||
|
||||
|
||||
### 📦 Package manager
|
||||
### 📦 Development Environment
|
||||
|
||||
We use `poetry` as our package manager. You can install poetry by following the instructions [here](https://python-poetry.org/docs/#installation).
|
||||
|
||||
Please DO NOT use pip or conda to install the dependencies. Instead, use poetry:
|
||||
We use `hatch` for managing development environments. To set up:
|
||||
|
||||
```bash
|
||||
poetry install --all-extras
|
||||
or
|
||||
poetry install --with dev
|
||||
# Activate environment for specific Python version:
|
||||
hatch shell dev_py_3_9 # Python 3.9
|
||||
hatch shell dev_py_3_10 # Python 3.10
|
||||
hatch shell dev_py_3_11 # Python 3.11
|
||||
hatch shell dev_py_3_12 # Python 3.12
|
||||
|
||||
#activate
|
||||
|
||||
poetry shell
|
||||
# The environment will automatically install all dev dependencies
|
||||
# Run tests within the activated shell:
|
||||
make test
|
||||
```
|
||||
|
||||
### 📌 Pre-commit
|
||||
@@ -41,34 +40,52 @@ To ensure our standards, make sure to install pre-commit before starting to cont
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
### 🧹 Linting
|
||||
|
||||
We use `ruff` to lint our code. You can run the linter by running the following command:
|
||||
|
||||
```bash
|
||||
make lint
|
||||
```
|
||||
|
||||
Make sure that the linter does not report any errors or warnings before submitting a pull request.
|
||||
|
||||
### Code Formatting with `black`
|
||||
|
||||
We use `black` to reformat the code by running the following command:
|
||||
|
||||
```bash
|
||||
make format
|
||||
```
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
We use `pytest` to test our code. You can run the tests by running the following command:
|
||||
We use `pytest` to test our code across multiple Python versions. You can run tests using:
|
||||
|
||||
```bash
|
||||
poetry run pytest
|
||||
# Run tests with default Python version
|
||||
make test
|
||||
|
||||
# Test specific Python versions:
|
||||
make test-py-3.9 # Python 3.9 environment
|
||||
make test-py-3.10 # Python 3.10 environment
|
||||
make test-py-3.11 # Python 3.11 environment
|
||||
make test-py-3.12 # Python 3.12 environment
|
||||
|
||||
# When using hatch shells, run tests with:
|
||||
make test # After activating a shell with hatch shell test_XX
|
||||
```
|
||||
|
||||
Make sure that all tests pass before submitting a pull request.
|
||||
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||
|
||||
## 🚀 Release Process
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
|
||||
At the moment, the release process is manual. We try to make frequent releases. Usually, we release a new version when we have a new feature or bugfix. A developer with admin rights to the repository will create a new release on GitHub, and then publish the new version to PyPI.
|
||||
### 🚀 Releasing
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
|
||||
|
||||
#### Tag Prefixes
|
||||
|
||||
| Package | Registry | Tag Prefix | Example |
|
||||
|---------|----------|------------|---------|
|
||||
| `mem0ai` (Python SDK) | PyPI | `v*` | `v0.1.31` |
|
||||
| `mem0-cli` (Python CLI) | PyPI | `cli-v*` | `cli-v0.2.1` |
|
||||
| `mem0ai` (TypeScript SDK) | npm | `ts-v*` | `ts-v2.4.6` |
|
||||
| `@mem0/cli` (Node CLI) | npm | `cli-node-v*` | `cli-node-v0.1.2` |
|
||||
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
|
||||
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
|
||||
|
||||
#### How to Release
|
||||
|
||||
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
|
||||
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
|
||||
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
|
||||
|
||||
#### Publishing Details
|
||||
|
||||
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
|
||||
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
|
||||
- All workflows require `permissions: id-token: write` for OIDC authentication
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
# Migration Guide: Upgrading to mem0 1.0.0
|
||||
|
||||
## TL;DR
|
||||
|
||||
**What changed?** We simplified the API by removing confusing version parameters. Now everything returns a consistent format: `{"results": [...]}`.
|
||||
|
||||
**What you need to do:**
|
||||
1. Upgrade: `pip install mem0ai==1.0.0`
|
||||
2. Remove `version` and `output_format` parameters from your code
|
||||
3. Update response handling to use `result["results"]` instead of treating responses as lists
|
||||
|
||||
**Time needed:** ~5-10 minutes for most projects
|
||||
|
||||
---
|
||||
|
||||
## Quick Migration Guide
|
||||
|
||||
### 1. Install the Update
|
||||
|
||||
```bash
|
||||
pip install mem0ai==1.0.0
|
||||
```
|
||||
|
||||
### 2. Update Your Code
|
||||
|
||||
**If you're using the Memory API:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
memory = Memory(config=MemoryConfig(version="v1.1"))
|
||||
result = memory.add("I like pizza")
|
||||
|
||||
# After
|
||||
memory = Memory() # That's it - version is automatic now
|
||||
result = memory.add("I like pizza")
|
||||
```
|
||||
|
||||
**If you're using the Client API:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
client.add(messages, output_format="v1.1")
|
||||
client.search(query, version="v2", output_format="v1.1")
|
||||
|
||||
# After
|
||||
client.add(messages) # Just remove those extra parameters
|
||||
client.search(query)
|
||||
```
|
||||
|
||||
### 3. Update How You Handle Responses
|
||||
|
||||
All responses now use the same format: a dictionary with `"results"` key.
|
||||
|
||||
```python
|
||||
# Before - you might have done this
|
||||
result = memory.add("I like pizza")
|
||||
for item in result: # Treating it as a list
|
||||
print(item)
|
||||
|
||||
# After - do this instead
|
||||
result = memory.add("I like pizza")
|
||||
for item in result["results"]: # Access the results key
|
||||
print(item)
|
||||
|
||||
# Graph relations (if you use them)
|
||||
if "relations" in result:
|
||||
for relation in result["relations"]:
|
||||
print(relation)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enhanced Message Handling
|
||||
|
||||
The platform client (MemoryClient) now supports the same flexible message formats as the OSS version:
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-key")
|
||||
|
||||
# All three formats now work:
|
||||
|
||||
# 1. Single string (automatically converted to user message)
|
||||
client.add("I like pizza", user_id="alice")
|
||||
|
||||
# 2. Single message dictionary
|
||||
client.add({"role": "user", "content": "I like pizza"}, user_id="alice")
|
||||
|
||||
# 3. List of messages (conversation)
|
||||
client.add([
|
||||
{"role": "user", "content": "I like pizza"},
|
||||
{"role": "assistant", "content": "I'll remember that!"}
|
||||
], user_id="alice")
|
||||
```
|
||||
|
||||
### Async Mode Configuration
|
||||
|
||||
The `async_mode` parameter now defaults to `True` but can be configured:
|
||||
|
||||
```python
|
||||
# Default behavior (async_mode=True)
|
||||
client.add(messages, user_id="alice")
|
||||
|
||||
# Explicitly set async mode
|
||||
client.add(messages, user_id="alice", async_mode=True)
|
||||
|
||||
# Disable async mode if needed
|
||||
client.add(messages, user_id="alice", async_mode=False)
|
||||
```
|
||||
|
||||
**Note:** `async_mode=True` provides better performance for most use cases. Only set it to `False` if you have specific synchronous processing requirements.
|
||||
|
||||
---
|
||||
|
||||
## That's It!
|
||||
|
||||
For most users, that's all you need to know. The changes are:
|
||||
- ✅ No more `version` or `output_format` parameters
|
||||
- ✅ Consistent `{"results": [...]}` response format
|
||||
- ✅ Cleaner, simpler API
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
**Getting `KeyError: 'results'`?**
|
||||
|
||||
Your code is still treating the response as a list. Update it:
|
||||
```python
|
||||
# Change this:
|
||||
for memory in response:
|
||||
|
||||
# To this:
|
||||
for memory in response["results"]:
|
||||
```
|
||||
|
||||
**Getting `TypeError: unexpected keyword argument`?**
|
||||
|
||||
You're still passing old parameters. Remove them:
|
||||
```python
|
||||
# Change this:
|
||||
client.add(messages, output_format="v1.1")
|
||||
|
||||
# To this:
|
||||
client.add(messages)
|
||||
```
|
||||
|
||||
**Seeing deprecation warnings?**
|
||||
|
||||
Remove any explicit `version="v1.0"` from your config:
|
||||
```python
|
||||
# Change this:
|
||||
memory = Memory(config=MemoryConfig(version="v1.0"))
|
||||
|
||||
# To this:
|
||||
memory = Memory()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's New in 1.0.0
|
||||
|
||||
- **Better vector stores:** Fixed OpenSearch and improved reliability across all stores
|
||||
- **Cleaner API:** One way to do things, no more confusing options
|
||||
- **Enhanced GCP support:** Better Vertex AI configuration options
|
||||
- **Flexible message input:** Platform client now accepts strings, dicts, and lists (aligned with OSS)
|
||||
- **Configurable async_mode:** Now defaults to `True` but users can override if needed
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check [GitHub Issues](https://github.com/mem0ai/mem0/issues)
|
||||
- Read the [documentation](https://docs.mem0.ai/)
|
||||
- Open a new issue if you're stuck
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Configuration Changes
|
||||
|
||||
**If you configured vector stores with version:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
config = MemoryConfig(
|
||||
version="v1.1",
|
||||
vector_store=VectorStoreConfig(...)
|
||||
)
|
||||
|
||||
# After
|
||||
config = MemoryConfig(
|
||||
vector_store=VectorStoreConfig(...)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Migration
|
||||
|
||||
Quick sanity check:
|
||||
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
memory = Memory()
|
||||
|
||||
# Add should return a dict with "results"
|
||||
result = memory.add("I like pizza", user_id="test")
|
||||
assert "results" in result
|
||||
|
||||
# Search should return a dict with "results"
|
||||
search = memory.search("food", user_id="test")
|
||||
assert "results" in search
|
||||
|
||||
# Get all should return a dict with "results"
|
||||
all_memories = memory.get_all(user_id="test")
|
||||
assert "results" in all_memories
|
||||
|
||||
print("✅ Migration successful!")
|
||||
```
|
||||
@@ -1,45 +1,52 @@
|
||||
# Variables
|
||||
PYTHON := python3
|
||||
PIP := $(PYTHON) -m pip
|
||||
PROJECT_NAME := embedchain
|
||||
.PHONY: format sort lint
|
||||
|
||||
# Targets
|
||||
.PHONY: install format lint clean test ci_lint ci_test coverage
|
||||
# Variables
|
||||
ISORT_OPTIONS = --profile black
|
||||
PROJECT_NAME := mem0ai
|
||||
|
||||
# Default target
|
||||
all: format sort lint
|
||||
|
||||
install:
|
||||
poetry install
|
||||
hatch env create
|
||||
|
||||
install_all:
|
||||
poetry install --all-extras
|
||||
|
||||
install_es:
|
||||
poetry install --extras elasticsearch
|
||||
|
||||
install_opensearch:
|
||||
poetry install --extras opensearch
|
||||
|
||||
install_milvus:
|
||||
poetry install --extras milvus
|
||||
|
||||
shell:
|
||||
poetry shell
|
||||
|
||||
py_shell:
|
||||
poetry run python
|
||||
pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
||||
google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \
|
||||
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk valkey
|
||||
|
||||
# Format code with ruff
|
||||
format:
|
||||
$(PYTHON) -m black .
|
||||
$(PYTHON) -m isort .
|
||||
hatch run format
|
||||
|
||||
# Sort imports with isort
|
||||
sort:
|
||||
hatch run isort mem0/
|
||||
|
||||
# Lint code with ruff
|
||||
lint:
|
||||
hatch run lint
|
||||
|
||||
docs:
|
||||
cd docs && mintlify dev
|
||||
|
||||
build:
|
||||
hatch build
|
||||
|
||||
publish:
|
||||
hatch publish
|
||||
|
||||
clean:
|
||||
rm -rf dist build *.egg-info
|
||||
rm -rf dist
|
||||
|
||||
lint:
|
||||
poetry run ruff .
|
||||
|
||||
# for example: make test file=tests/test_factory.py
|
||||
test:
|
||||
poetry run pytest $(file)
|
||||
hatch run test
|
||||
|
||||
coverage:
|
||||
poetry run pytest --cov=$(PROJECT_NAME) --cov-report=xml
|
||||
test-py-3.10:
|
||||
hatch run dev_py_3_10:test
|
||||
|
||||
test-py-3.11:
|
||||
hatch run dev_py_3_11:test
|
||||
|
||||
test-py-3.12:
|
||||
hatch run dev_py_3_12:test
|
||||
|
||||
@@ -1,132 +1,249 @@
|
||||
<p align="center">
|
||||
<img src="docs/logo/dark.svg" width="400px" alt="Embedchain Logo">
|
||||
<a href="https://github.com/mem0ai/mem0">
|
||||
<img src="docs/images/banner-sm.png" width="800px" alt="Mem0 - The Memory Layer for Personalized AI">
|
||||
</a>
|
||||
</p>
|
||||
<p align="center" style="display: flex; justify-content: center; gap: 20px; align-items: center;">
|
||||
<a href="https://trendshift.io/repositories/11194" target="blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/11194" alt="mem0ai%2Fmem0 | Trendshift" width="250" height="55"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://runacap.com/ross-index/q3-2023/" target="_blank" rel="noopener"><img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2023/10/ROSS_badge_black_Q3_2023.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q3 2023 | Runa Capital" width="260" height="56"/></a>
|
||||
<a href="https://mem0.ai">Learn more</a>
|
||||
·
|
||||
<a href="https://mem0.dev/DiG">Join Discord</a>
|
||||
·
|
||||
<a href="https://mem0.dev/demo">Demo</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://pypi.org/project/embedchain/">
|
||||
<img src="https://img.shields.io/pypi/v/embedchain" alt="PyPI">
|
||||
<a href="https://mem0.dev/DiG">
|
||||
<img src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" alt="Mem0 Discord">
|
||||
</a>
|
||||
<a href="https://pepy.tech/project/embedchain">
|
||||
<img src="https://static.pepy.tech/badge/embedchain" alt="Downloads">
|
||||
<a href="https://pepy.tech/project/mem0ai">
|
||||
<img src="https://img.shields.io/pypi/dm/mem0ai" alt="Mem0 PyPI - Downloads">
|
||||
</a>
|
||||
<a href="https://embedchain.ai/slack">
|
||||
<img src="https://img.shields.io/badge/slack-embedchain-brightgreen.svg?logo=slack" alt="Slack">
|
||||
<a href="https://github.com/mem0ai/mem0">
|
||||
<img src="https://img.shields.io/github/commit-activity/m/mem0ai/mem0?style=flat-square" alt="GitHub commit activity">
|
||||
</a>
|
||||
<a href="https://embedchain.ai/discord">
|
||||
<img src="https://dcbadge.vercel.app/api/server/6PzXDgEjG5?style=flat" alt="Discord">
|
||||
<a href="https://pypi.org/project/mem0ai" target="blank">
|
||||
<img src="https://img.shields.io/pypi/v/mem0ai?color=%2334D058&label=pypi%20package" alt="Package version">
|
||||
</a>
|
||||
<a href="https://twitter.com/embedchain">
|
||||
<img src="https://img.shields.io/twitter/follow/embedchain" alt="Twitter">
|
||||
<a href="https://www.npmjs.com/package/mem0ai" target="blank">
|
||||
<img src="https://img.shields.io/npm/v/mem0ai" alt="Npm package">
|
||||
</a>
|
||||
<a href="https://colab.research.google.com/drive/138lMWhENGeEu7Q1-6lNbNTHGLZXBBz_B?usp=sharing">
|
||||
<img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open in Colab">
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/embedchain/embedchain">
|
||||
<img src="https://codecov.io/gh/embedchain/embedchain/graph/badge.svg?token=EMRRHZXW1Q" alt="codecov">
|
||||
<a href="https://www.ycombinator.com/companies/mem0">
|
||||
<img src="https://img.shields.io/badge/Y%20Combinator-S24-orange?style=flat-square" alt="Y Combinator S24">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
<p align="center">
|
||||
<a href="https://mem0.ai/research"><strong>📄 Benchmarking Mem0's token-efficient memory algorithm →</strong></a>
|
||||
</p>
|
||||
|
||||
## New Memory Algorithm (April 2026)
|
||||
|
||||
> ### Checkout our latest [Sadhguru AI app](https://sadhguru-ai.streamlit.app/) built using Embedchain.
|
||||
| Benchmark | Old | New | Tokens | Latency p50 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
|
||||
| **LongMemEval** | 67.8 | **94.8** | 6.8K | 1.09s |
|
||||
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
|
||||
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
|
||||
|
||||
## What is Embedchain?
|
||||
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops).
|
||||
|
||||
Embedchain is an Open Source RAG Framework that makes it easy to create and deploy AI apps. At its core, Embedchain follows the design principle of being *"Conventional but Configurable"* to serve both software engineers and machine learning engineers.
|
||||
**What changed:**
|
||||
- **Single-pass ADD-only extraction** -- one LLM call, no UPDATE/DELETE. Memories accumulate; nothing is overwritten.
|
||||
- **Agent-generated facts are first-class** -- when an agent confirms an action, that information is now stored with equal weight.
|
||||
- **Entity linking** -- entities are extracted, embedded, and linked across memories for retrieval boosting.
|
||||
- **Multi-signal retrieval** -- semantic, BM25 keyword, and entity matching scored in parallel and fused.
|
||||
- **Temporal Reasoning** -- time-aware retrieval that ranks the right dated instance for queries about current state, past events, and upcoming plans.
|
||||
|
||||
Embedchain streamlines the creation of Retrieval-Augmented Generation (RAG) applications, offering a seamless process for managing various types of unstructured data. It efficiently segments data into manageable chunks, generates relevant embeddings, and stores them in a vector database for optimized retrieval. With a suite of diverse APIs, it enables users to extract contextual information, find precise answers, or engage in interactive chat conversations, all tailored to their own data.
|
||||
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
|
||||
|
||||
## 🔧 Quick install
|
||||
## Research Highlights
|
||||
- **91.6 on LoCoMo** -- +20 points over the previous algorithm
|
||||
- **94.8 on LongMemEval** -- +27 points, with +53.6 on assistant memory recall
|
||||
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
|
||||
- [Read the full paper](https://mem0.ai/research)
|
||||
|
||||
### Python API
|
||||
# Introduction
|
||||
|
||||
[Mem0](https://mem0.ai) ("mem-zero") enhances AI assistants and agents with an intelligent memory layer, enabling personalized AI interactions. It remembers user preferences, adapts to individual needs, and continuously learns over time—ideal for customer support chatbots, AI assistants, and autonomous systems.
|
||||
|
||||
### Key Features & Use Cases
|
||||
|
||||
**Core Capabilities:**
|
||||
- **Multi-Level Memory**: Seamlessly retains User, Session, and Agent state with adaptive personalization
|
||||
- **Developer-Friendly**: Intuitive API, cross-platform SDKs, and a fully managed service option
|
||||
|
||||
**Applications:**
|
||||
- **AI Assistants**: Consistent, context-rich conversations
|
||||
- **Customer Support**: Recall past tickets and user history for tailored help
|
||||
- **Healthcare**: Track patient preferences and history for personalized care
|
||||
- **Productivity & Gaming**: Adaptive workflows and environments based on user behavior
|
||||
|
||||
## 🚀 Quickstart Guide <a name="quickstart"></a>
|
||||
|
||||
> **Agent Mode — for AI agents setting up Mem0 autonomously.** Run `mem0 init --agent --agent-caller <your-name> --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`), replacing `<your-name>` with your AI agent identity (e.g. `claude-code`, `cursor`, `codex`). This mints an evaluation Mem0 API key in <5 seconds — no email, no dashboard, no OTP. If you forgot to pass `--agent-caller`, run `mem0 identify <your-name>` after init. Use the key immediately against the Platform or the MCP server. The human owner can claim ownership later with `mem0 init --email <their-email>`: memories transfer, the same key keeps working, and the agent isn't disrupted.
|
||||
|
||||
| | Library | Self-Hosted Server | Cloud Platform |
|
||||
|---|---------|-------------------|----------------|
|
||||
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
|
||||
| **Setup** | `pip install mem0ai` | `docker compose up` | Sign up at [app.mem0.ai](https://app.mem0.ai?utm_source=oss&utm_medium=readme) |
|
||||
| **Dashboard** | -- | [Yes](https://docs.mem0.ai/open-source/setup) | Yes |
|
||||
| **Auth & API Keys** | -- | Yes | Yes |
|
||||
| **Advanced Features** | -- | Teasers | All included |
|
||||
|
||||
Just testing? Use the library. Building for a team? Self-hosted. Want zero ops? Cloud.
|
||||
|
||||
### Library (pip / npm)
|
||||
|
||||
```bash
|
||||
pip install embedchain
|
||||
pip install mem0ai
|
||||
```
|
||||
|
||||
## ✨ Live demo
|
||||
For enhanced hybrid search with BM25 keyword matching and entity extraction, install with NLP support:
|
||||
|
||||
Checkout the [Chat with PDF](https://embedchain.ai/demo/chat-pdf) live demo we created using Embedchain. You can find the source code [here](https://github.com/embedchain/embedchain/tree/main/examples/chat-pdf).
|
||||
```bash
|
||||
pip install mem0ai[nlp]
|
||||
python -m spacy download en_core_web_sm
|
||||
```
|
||||
|
||||
## 🔍 Usage
|
||||
Install sdk via npm:
|
||||
|
||||
<!-- Demo GIF or Image -->
|
||||
<p align="center">
|
||||
<img src="docs/images/cover.gif" width="900px" alt="Embedchain Demo">
|
||||
</p>
|
||||
```bash
|
||||
npm install mem0ai
|
||||
```
|
||||
|
||||
For example, you can create an Elon Musk bot using the following code:
|
||||
### Self-Hosted Server
|
||||
|
||||
> **Note:** Self-hosted auth is on by default. Upgrading from a pre-auth build? Set `ADMIN_API_KEY`, register an admin through the wizard, or `AUTH_DISABLED=true` for local dev only. See [upgrade notes](https://docs.mem0.ai/open-source/setup#upgrade-notes).
|
||||
|
||||
```bash
|
||||
# Recommended: one command — start the stack, create an admin, issue the first API key.
|
||||
cd server && make bootstrap
|
||||
|
||||
# Manual: start the stack and finish setup via the browser wizard.
|
||||
cd server && docker compose up -d # http://localhost:3000
|
||||
```
|
||||
|
||||
See the [self-hosted docs](https://docs.mem0.ai/open-source/overview) for configuration.
|
||||
|
||||
### Cloud Platform
|
||||
|
||||
1. Sign up on [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=readme)
|
||||
2. Embed the memory layer via SDK or API keys
|
||||
|
||||
### CLI
|
||||
|
||||
Manage memories from your terminal:
|
||||
|
||||
```bash
|
||||
npm install -g @mem0/cli # or: pip install mem0-cli
|
||||
|
||||
mem0 init
|
||||
mem0 add "Prefers dark mode and vim keybindings" --user-id alice
|
||||
mem0 search "What does Alice prefer?" --user-id alice
|
||||
```
|
||||
|
||||
See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full command reference.
|
||||
|
||||
### Agent Skills
|
||||
|
||||
Teach your AI coding assistant (Claude Code, Codex, Cursor, Windsurf, OpenCode, OpenClaw, and any tool that supports the skills standard) how to build with Mem0. Two categories:
|
||||
|
||||
**Reference skills — always on** (SDK knowledge loaded into the assistant's context):
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-cli
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
|
||||
```
|
||||
|
||||
**Pipeline skills — run on demand** (execute an end-to-end workflow in an existing repo):
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
|
||||
```
|
||||
|
||||
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Mem0 requires an LLM to function, with `gpt-5-mini` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
||||
|
||||
Mem0 uses `text-embedding-3-small` from OpenAI as the default embedding model. For best results with hybrid search (semantic + keyword + entity boosting), we recommend using at least [Qwen 600M](https://huggingface.co/Alibaba-NLP/gte-Qwen2-1.5B-instruct) or a comparable embedding model. See [Supported Embeddings](https://docs.mem0.ai/components/embedders/overview) for configuration details.
|
||||
|
||||
First step is to instantiate the memory:
|
||||
|
||||
```python
|
||||
import os
|
||||
from embedchain import Pipeline as App
|
||||
from openai import OpenAI
|
||||
from mem0 import Memory
|
||||
|
||||
# Create a bot instance
|
||||
os.environ["OPENAI_API_KEY"] = "YOUR API KEY"
|
||||
elon_bot = App()
|
||||
openai_client = OpenAI()
|
||||
memory = Memory()
|
||||
|
||||
# Embed online resources
|
||||
elon_bot.add("https://en.wikipedia.org/wiki/Elon_Musk")
|
||||
elon_bot.add("https://www.forbes.com/profile/elon-musk")
|
||||
def chat_with_memories(message: str, user_id: str = "default_user") -> str:
|
||||
# Retrieve relevant memories
|
||||
relevant_memories = memory.search(query=message, filters={"user_id": user_id}, top_k=3)
|
||||
memories_str = "\n".join(f"- {entry['memory']}" for entry in relevant_memories["results"])
|
||||
|
||||
# Query the bot
|
||||
elon_bot.query("How many companies does Elon Musk run and name those?")
|
||||
# Answer: Elon Musk currently runs several companies. As of my knowledge, he is the CEO and lead designer of SpaceX, the CEO and product architect of Tesla, Inc., the CEO and founder of Neuralink, and the CEO and founder of The Boring Company. However, please note that this information may change over time, so it's always good to verify the latest updates.
|
||||
# Generate Assistant response
|
||||
system_prompt = f"You are a helpful AI. Answer the question based on query and memories.\nUser Memories:\n{memories_str}"
|
||||
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": message}]
|
||||
response = openai_client.chat.completions.create(model="gpt-5-mini", messages=messages)
|
||||
assistant_response = response.choices[0].message.content
|
||||
|
||||
# Create new memories from the conversation
|
||||
messages.append({"role": "assistant", "content": assistant_response})
|
||||
memory.add(messages, user_id=user_id)
|
||||
|
||||
return assistant_response
|
||||
|
||||
def main():
|
||||
print("Chat with AI (type 'exit' to quit)")
|
||||
while True:
|
||||
user_input = input("You: ").strip()
|
||||
if user_input.lower() == 'exit':
|
||||
print("Goodbye!")
|
||||
break
|
||||
print(f"AI: {chat_with_memories(user_input)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
You can also try it in your browser with Google Colab:
|
||||
For detailed integration steps, see the [Quickstart](https://docs.mem0.ai/quickstart) and [API Reference](https://docs.mem0.ai/api-reference).
|
||||
|
||||
[](https://colab.research.google.com/drive/17ON1LPonnXAtLaZEebnOktstB_1cJJmh?usp=sharing)
|
||||
## 🔗 Integrations & Demos
|
||||
|
||||
## 📖 Documentation
|
||||
Comprehensive guides and API documentation are available to help you get the most out of Embedchain:
|
||||
- **ChatGPT with Memory**: Personalized chat powered by Mem0 ([Live Demo](https://mem0.dev/demo))
|
||||
- **Browser Extension**: Store memories across ChatGPT, Perplexity, and Claude ([Chrome Extension](https://chromewebstore.google.com/detail/onihkkbipkfeijkadecaafbgagkhglop?utm_source=item-share-cb))
|
||||
- **Langgraph Support**: Build a customer bot with Langgraph + Mem0 ([Guide](https://docs.mem0.ai/integrations/langgraph))
|
||||
- **CrewAI Integration**: Tailor CrewAI outputs with Mem0 ([Example](https://docs.mem0.ai/integrations/crewai))
|
||||
|
||||
- [Introduction](https://docs.embedchain.ai/get-started/introduction#what-is-embedchain)
|
||||
- [Getting Started](https://docs.embedchain.ai/get-started/quickstart)
|
||||
- [Examples](https://docs.embedchain.ai/examples)
|
||||
- [Supported data types](https://docs.embedchain.ai/components/data-sources/overview)
|
||||
## 📚 Documentation & Support
|
||||
|
||||
## 🔗 Join the Community
|
||||
|
||||
* Connect with fellow developers by joining our [Slack Community](https://embedchain.ai/slack) or [Discord Community](https://embedchain.ai/discord).
|
||||
|
||||
* Dive into [GitHub Discussions](https://github.com/embedchain/embedchain/discussions), ask questions, or share your experiences.
|
||||
|
||||
## 🤝 Schedule a 1-on-1 Session
|
||||
|
||||
Book a [1-on-1 Session](https://cal.com/taranjeetio/ec) with the founders, to discuss any issues, provide feedback, or explore how we can improve Embedchain for you.
|
||||
|
||||
## 🌐 Contributing
|
||||
|
||||
Contributions are welcome! Please check out the issues on the repository, and feel free to open a pull request.
|
||||
For more information, please see the [contributing guidelines](CONTRIBUTING.md).
|
||||
|
||||
For more reference, please go through [Development Guide](https://docs.embedchain.ai/contribution/dev) and [Documentation Guide](https://docs.embedchain.ai/contribution/docs).
|
||||
|
||||
<a href="https://github.com/embedchain/embedchain/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=embedchain/embedchain" />
|
||||
</a>
|
||||
|
||||
## Anonymous Telemetry
|
||||
|
||||
We collect anonymous usage metrics to enhance our package's quality and user experience. This includes data like feature usage frequency and system info, but never personal details. The data helps us prioritize improvements and ensure compatibility. If you wish to opt-out, set the environment variable `EC_TELEMETRY=false`. We prioritize data security and don't share this data externally.
|
||||
- Full docs: https://docs.mem0.ai
|
||||
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai)
|
||||
- Contact: founders@mem0.ai
|
||||
|
||||
## Citation
|
||||
|
||||
If you utilize this repository, please consider citing it with:
|
||||
We now have a paper you can cite:
|
||||
|
||||
```
|
||||
@misc{embedchain,
|
||||
author = {Taranjeet Singh, Deshraj Yadav},
|
||||
title = {Embedchain: The Open Source RAG Framework},
|
||||
year = {2023},
|
||||
publisher = {GitHub},
|
||||
journal = {GitHub repository},
|
||||
howpublished = {\url{https://github.com/embedchain/embedchain}},
|
||||
```bibtex
|
||||
@article{mem0,
|
||||
title={Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory},
|
||||
author={Chhikara, Prateek and Khant, Dev and Aryan, Saket and Singh, Taranjeet and Yadav, Deshraj},
|
||||
journal={arXiv preprint arXiv:2504.19413},
|
||||
year={2025}
|
||||
}
|
||||
```
|
||||
|
||||
## ⚖️ License
|
||||
|
||||
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+136
@@ -0,0 +1,136 @@
|
||||
# mem0 CLI
|
||||
|
||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Works with the Mem0 Platform API. Available in Python and Node.js.
|
||||
|
||||
> **For AI agents:** pass `--agent` (or `--json`) on any command for structured JSON output purpose-built for tool loops — sanitized fields, no colors or spinners, errors as JSON. See [Agent mode](#agent-mode) below.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @mem0/cli
|
||||
```
|
||||
|
||||
```bash
|
||||
pip install mem0-cli
|
||||
```
|
||||
|
||||
Both packages install a `mem0` binary with identical behavior.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Interactive setup wizard
|
||||
mem0 init
|
||||
|
||||
# Or login via email (get a new API key)
|
||||
mem0 init --email alice@company.com
|
||||
|
||||
# Or authenticate with an existing API key
|
||||
mem0 init --api-key m0-xxx
|
||||
|
||||
# Add a memory
|
||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||
|
||||
# Search memories
|
||||
mem0 search "What are Alice's preferences?" --user-id alice
|
||||
|
||||
# List all memories for a user
|
||||
mem0 list --user-id alice
|
||||
|
||||
# Update a memory
|
||||
mem0 update <memory-id> "I switched to light mode"
|
||||
|
||||
# Delete a memory
|
||||
mem0 delete <memory-id>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `mem0 init` | Setup wizard — login via email or configure API key manually |
|
||||
| `mem0 add` | Add a memory from text, JSON messages, a file, or stdin |
|
||||
| `mem0 search` | Search memories using natural language |
|
||||
| `mem0 list` | List memories with optional filters and pagination |
|
||||
| `mem0 get` | Retrieve a specific memory by ID |
|
||||
| `mem0 update` | Update the text or metadata of a memory |
|
||||
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
|
||||
| `mem0 import` | Bulk import memories from a JSON file |
|
||||
| `mem0 config` | View or modify CLI configuration |
|
||||
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
|
||||
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
|
||||
| `mem0 status` | Verify API connection and display current project |
|
||||
| `mem0 version` | Print the CLI version |
|
||||
|
||||
Run `mem0 <command> --help` for detailed usage on any command.
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
|
||||
```bash
|
||||
mem0 --agent search "user preferences" --user-id alice
|
||||
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||
mem0 --agent list --user-id alice
|
||||
```
|
||||
|
||||
Every command returns the same envelope shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "search",
|
||||
"duration_ms": 134,
|
||||
"scope": { "user_id": "alice" },
|
||||
"count": 2,
|
||||
"data": [
|
||||
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
What agent mode does differently from `--output json`:
|
||||
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||
|
||||
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||
|
||||
## Output formats
|
||||
|
||||
Control how results are displayed with `--output`:
|
||||
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `text` | Human-readable with colors and formatting (default) |
|
||||
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||
| `table` | Tabular format (default for `list`) |
|
||||
| `quiet` | Minimal — just IDs or status codes |
|
||||
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||
| `MEM0_BASE_URL` | API base URL |
|
||||
| `MEM0_USER_ID` | Default user ID |
|
||||
| `MEM0_AGENT_ID` | Default agent ID |
|
||||
| `MEM0_APP_ID` | Default app ID |
|
||||
| `MEM0_RUN_ID` | Default run ID |
|
||||
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||
|
||||
## Implementations
|
||||
|
||||
| Language | Directory | Package | Docs |
|
||||
|----------|-----------|---------|------|
|
||||
| TypeScript | [`node/`](./node/) | `@mem0/cli` | [README](./node/README.md) |
|
||||
| Python | [`python/`](./python/) | `mem0-cli` | [README](./python/README.md) |
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,551 @@
|
||||
{
|
||||
"specVersion": 1,
|
||||
"cli": {
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "The Memory Layer for AI Agents",
|
||||
"helpText": "\u25c6 mem0 CLI \u2014 The Memory Layer for AI Agents"
|
||||
},
|
||||
"branding": {
|
||||
"logoMini": "\u25c6 mem0",
|
||||
"tagline": "The Memory Layer for AI Agents",
|
||||
"colors": {
|
||||
"brand": "#8b5cf6",
|
||||
"accent": "#a78bfa",
|
||||
"success": "#22c55e",
|
||||
"error": "#ef4444",
|
||||
"warning": "#f59e0b",
|
||||
"dim": "#6b7280"
|
||||
},
|
||||
"icons": {
|
||||
"success": "\u2713",
|
||||
"error": "\u2717",
|
||||
"warning": "\u26a0",
|
||||
"info": "\u25c6",
|
||||
"pending": "\u29d7",
|
||||
"add": "+",
|
||||
"update": "~",
|
||||
"delete": "-",
|
||||
"noop": "\u00b7",
|
||||
"connected": "\u25cf",
|
||||
"disconnected": "\u25cf"
|
||||
},
|
||||
"logo": "███\u2557 ███\u2557███████\u2557███\u2557 ███\u2557 ██████\u2557 ██████\u2557██\u2557 ██\u2557\n████\u2557 ████\u2551██\u2554\u2550\u2550\u2550\u2550\u255d████\u2557 ████\u2551██\u2554\u2550████\u2557 ██\u2554\u2550\u2550\u2550\u2550\u255d██\u2551 ██\u2551\n██\u2554████\u2554██\u2551█████\u2557 ██\u2554████\u2554██\u2551██\u2551██\u2554██\u2551 ██\u2551 ██\u2551 ██\u2551\n██\u2551\u255a██\u2554\u255d██\u2551██\u2554\u2550\u2550\u255d ██\u2551\u255a██\u2554\u255d██\u2551████\u2554\u255d██\u2551 ██\u2551 ██\u2551 ██\u2551\n██\u2551 \u255a\u2550\u255d ██\u2551███████\u2557██\u2551 \u255a\u2550\u255d ██\u2551\u255a██████\u2554\u255d \u255a██████\u2557███████\u2557██\u2551\n\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d"
|
||||
},
|
||||
"config": {
|
||||
"configDir": "~/.mem0",
|
||||
"configFile": "config.json",
|
||||
"version": 1,
|
||||
"defaultBaseUrl": "https://api.mem0.ai",
|
||||
"sections": {
|
||||
"platform": {
|
||||
"fields": {
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"envVar": "MEM0_API_KEY",
|
||||
"redact": true
|
||||
},
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"default": "https://api.mem0.ai",
|
||||
"envVar": "MEM0_BASE_URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"fields": {
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"envVar": "MEM0_USER_ID"
|
||||
},
|
||||
"agent_id": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"envVar": "MEM0_AGENT_ID"
|
||||
},
|
||||
"app_id": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"envVar": "MEM0_APP_ID"
|
||||
},
|
||||
"run_id": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"envVar": "MEM0_RUN_ID"
|
||||
},
|
||||
"enable_graph": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"envVar": "MEM0_ENABLE_GRAPH"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"endpoints": {
|
||||
"add": { "method": "POST", "path": "/v1/memories/" },
|
||||
"search": { "method": "POST", "path": "/v2/memories/search/" },
|
||||
"get": { "method": "GET", "path": "/v1/memories/{memory_id}/" },
|
||||
"list": { "method": "POST", "path": "/v2/memories/" },
|
||||
"update": { "method": "PUT", "path": "/v1/memories/{memory_id}/" },
|
||||
"delete": { "method": "DELETE", "path": "/v1/memories/{memory_id}/" },
|
||||
"deleteAll": { "method": "DELETE", "path": "/v1/memories/" },
|
||||
"entities": { "method": "GET", "path": "/v1/entities/" },
|
||||
"deleteEntities": { "method": "DELETE", "path": "/v1/entities/" }
|
||||
},
|
||||
"authHeader": "Authorization",
|
||||
"authScheme": "Token",
|
||||
"timeout": 30,
|
||||
"entityTypeMap": {
|
||||
"users": "user",
|
||||
"agents": "agent",
|
||||
"apps": "app",
|
||||
"runs": "run"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"AuthError": {
|
||||
"httpStatus": 401,
|
||||
"message": "Authentication failed. Your API key may be invalid or expired."
|
||||
},
|
||||
"NotFoundError": {
|
||||
"httpStatus": 404,
|
||||
"messageTemplate": "Resource not found: {path}"
|
||||
},
|
||||
"APIError": {
|
||||
"httpStatus": 400,
|
||||
"messageTemplate": "Bad request to {path}: {detail}"
|
||||
},
|
||||
"noApiKey": {
|
||||
"message": "No API key configured.",
|
||||
"hint": "Run 'mem0 init' or set MEM0_API_KEY environment variable."
|
||||
}
|
||||
},
|
||||
"optionGroups": {
|
||||
"scope": {
|
||||
"label": "Scope",
|
||||
"options": ["user_id", "agent_id", "app_id", "run_id"]
|
||||
},
|
||||
"search": {
|
||||
"label": "Search",
|
||||
"options": ["top_k", "threshold", "rerank", "keyword", "filter_json", "fields", "graph", "no_graph"]
|
||||
},
|
||||
"pagination": {
|
||||
"label": "Pagination",
|
||||
"options": ["page", "page_size"]
|
||||
},
|
||||
"filters": {
|
||||
"label": "Filters",
|
||||
"options": ["category", "after", "before", "graph", "no_graph"]
|
||||
},
|
||||
"output": {
|
||||
"label": "Output",
|
||||
"options": ["output"]
|
||||
},
|
||||
"connection": {
|
||||
"label": "Connection",
|
||||
"options": ["api_key", "base_url"]
|
||||
}
|
||||
},
|
||||
"globalOptions": [
|
||||
{
|
||||
"name": "api_key",
|
||||
"flags": ["--api-key"],
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"envVar": "MEM0_API_KEY",
|
||||
"help": "Override API key.",
|
||||
"panel": "Connection"
|
||||
},
|
||||
{
|
||||
"name": "base_url",
|
||||
"flags": ["--base-url"],
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"help": "Override API base URL.",
|
||||
"panel": "Connection"
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"flags": ["--version"],
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"help": "Show version and exit."
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"name": "add",
|
||||
"description": "Add a memory from text, messages, file, or stdin.",
|
||||
"usage": "mem0 add <text> [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": true,
|
||||
"resolveIds": true,
|
||||
"resolveGraph": true,
|
||||
"confirmDangerous": false,
|
||||
"outputFormats": ["text", "json", "quiet"],
|
||||
"defaultOutput": "text",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"help": "Text content to add as a memory."
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
|
||||
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Scope to agent.", "panel": "Scope" },
|
||||
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Scope to app.", "panel": "Scope" },
|
||||
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Scope to run.", "panel": "Scope" },
|
||||
{ "name": "messages", "flags": ["--messages"], "type": "string", "help": "Conversation messages as JSON." },
|
||||
{ "name": "file", "flags": ["--file", "-f"], "type": "path", "help": "Read messages from JSON file." },
|
||||
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Custom metadata as JSON." },
|
||||
{ "name": "immutable", "flags": ["--immutable"], "type": "boolean", "default": false, "help": "Prevent future updates." },
|
||||
{ "name": "no_infer", "flags": ["--no-infer"], "type": "boolean", "default": false, "help": "Skip inference, store raw." },
|
||||
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
|
||||
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Categories (JSON array or comma-separated)." },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph memory extraction.", "panel": "Scope" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph memory extraction.", "panel": "Scope" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output format: text, json, quiet.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "add"
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"description": "Search memories by semantic query.",
|
||||
"usage": "mem0 search <query> [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": true,
|
||||
"resolveIds": true,
|
||||
"resolveGraph": true,
|
||||
"confirmDangerous": false,
|
||||
"outputFormats": ["text", "json", "table"],
|
||||
"defaultOutput": "text",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "query",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"help": "Search query."
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Filter by user.", "panel": "Scope" },
|
||||
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Filter by agent.", "panel": "Scope" },
|
||||
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Filter by app.", "panel": "Scope" },
|
||||
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Filter by run.", "panel": "Scope" },
|
||||
{ "name": "top_k", "flags": ["--top-k", "-k", "--limit"], "type": "integer", "default": 10, "help": "Number of results.", "panel": "Search" },
|
||||
{ "name": "threshold", "flags": ["--threshold"], "type": "float", "default": 0.3, "help": "Minimum similarity score.", "panel": "Search" },
|
||||
{ "name": "rerank", "flags": ["--rerank"], "type": "boolean", "default": false, "help": "Enable reranking (Platform only).", "panel": "Search" },
|
||||
{ "name": "keyword", "flags": ["--keyword"], "type": "boolean", "default": false, "help": "Use keyword search.", "panel": "Search" },
|
||||
{ "name": "filter_json", "flags": ["--filter"], "type": "string", "help": "Advanced filter expression (JSON).", "panel": "Search" },
|
||||
{ "name": "fields", "flags": ["--fields"], "type": "string", "help": "Specific fields to return (comma-separated).", "panel": "Search" },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in search.", "panel": "Search" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in search.", "panel": "Search" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, table.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "search"
|
||||
},
|
||||
{
|
||||
"name": "get",
|
||||
"description": "Get a specific memory by ID.",
|
||||
"usage": "mem0 get <memory_id> [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": false,
|
||||
"resolveIds": false,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": false,
|
||||
"outputFormats": ["text", "json"],
|
||||
"defaultOutput": "text",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "memory_id",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"help": "Memory ID to retrieve."
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "get"
|
||||
},
|
||||
{
|
||||
"name": "list",
|
||||
"description": "List memories with optional filters.",
|
||||
"usage": "mem0 list [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": true,
|
||||
"resolveIds": true,
|
||||
"resolveGraph": true,
|
||||
"confirmDangerous": false,
|
||||
"outputFormats": ["text", "json", "table"],
|
||||
"defaultOutput": "table",
|
||||
"arguments": [],
|
||||
"options": [
|
||||
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Filter by user.", "panel": "Scope" },
|
||||
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Filter by agent.", "panel": "Scope" },
|
||||
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Filter by app.", "panel": "Scope" },
|
||||
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Filter by run.", "panel": "Scope" },
|
||||
{ "name": "page", "flags": ["--page"], "type": "integer", "default": 1, "help": "Page number.", "panel": "Pagination" },
|
||||
{ "name": "page_size", "flags": ["--page-size"], "type": "integer", "default": 100, "help": "Results per page.", "panel": "Pagination" },
|
||||
{ "name": "category", "flags": ["--category"], "type": "string", "help": "Filter by category.", "panel": "Filters" },
|
||||
{ "name": "after", "flags": ["--after"], "type": "string", "help": "Created after (YYYY-MM-DD).", "panel": "Filters" },
|
||||
{ "name": "before", "flags": ["--before"], "type": "string", "help": "Created before (YYYY-MM-DD).", "panel": "Filters" },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in listing.", "panel": "Filters" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in listing.", "panel": "Filters" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: text, json, table.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "list"
|
||||
},
|
||||
{
|
||||
"name": "update",
|
||||
"description": "Update a memory's text or metadata.",
|
||||
"usage": "mem0 update <memory_id> [text] [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": false,
|
||||
"resolveIds": false,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": false,
|
||||
"outputFormats": ["text", "json", "quiet"],
|
||||
"defaultOutput": "text",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "memory_id",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"help": "Memory ID to update."
|
||||
},
|
||||
{
|
||||
"name": "text",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"help": "New memory text."
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Update metadata (JSON)." },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "update"
|
||||
},
|
||||
{
|
||||
"name": "delete",
|
||||
"description": "Delete a memory, all memories matching a scope, or an entity.",
|
||||
"usage": "mem0 delete [memory_id] [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": true,
|
||||
"resolveIds": true,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": true,
|
||||
"outputFormats": ["text", "json", "quiet"],
|
||||
"defaultOutput": "text",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "memory_id",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"help": "Memory ID to delete (omit when using --all or --entity)."
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{ "name": "all", "flags": ["--all"], "type": "boolean", "default": false, "help": "Delete all memories matching scope filters." },
|
||||
{ "name": "entity", "flags": ["--entity"], "type": "boolean", "default": false, "help": "Delete the entity itself and all its memories (cascade)." },
|
||||
{ "name": "project", "flags": ["--project"], "type": "boolean", "default": false, "help": "With --all: delete ALL memories project-wide." },
|
||||
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
|
||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
|
||||
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
|
||||
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Scope to agent.", "panel": "Scope" },
|
||||
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Scope to app.", "panel": "Scope" },
|
||||
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Scope to run.", "panel": "Scope" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "delete",
|
||||
"notes": "Mutually exclusive modes: (1) mem0 delete <id> -- single memory, (2) mem0 delete --all [scope] -- bulk delete, (3) mem0 delete --entity [scope] -- entity cascade delete. Cannot combine <memoryId> with --all or --entity, and cannot combine --all with --entity."
|
||||
},
|
||||
{
|
||||
"name": "import",
|
||||
"description": "Import memories from a JSON file.",
|
||||
"usage": "mem0 import <file_path> [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": true,
|
||||
"resolveIds": true,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": false,
|
||||
"outputFormats": ["text"],
|
||||
"defaultOutput": "text",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "file_path",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"help": "JSON file to import."
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Override user ID.", "panel": "Scope" },
|
||||
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Override agent ID.", "panel": "Scope" }
|
||||
],
|
||||
"apiEndpoint": "add"
|
||||
},
|
||||
{
|
||||
"name": "config",
|
||||
"description": "Manage mem0 configuration.",
|
||||
"isGroup": true,
|
||||
"subcommands": [
|
||||
{
|
||||
"name": "show",
|
||||
"description": "Display current configuration (secrets redacted).",
|
||||
"usage": "mem0 config show",
|
||||
"needsBackend": false,
|
||||
"needsConfig": false,
|
||||
"arguments": [],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "get",
|
||||
"description": "Get a configuration value.",
|
||||
"usage": "mem0 config get <key>",
|
||||
"needsBackend": false,
|
||||
"needsConfig": false,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"help": "Config key (e.g. platform.api_key)."
|
||||
}
|
||||
],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "set",
|
||||
"description": "Set a configuration value.",
|
||||
"usage": "mem0 config set <key> <value>",
|
||||
"needsBackend": false,
|
||||
"needsConfig": false,
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"help": "Config key (e.g. platform.api_key)."
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"help": "Value to set."
|
||||
}
|
||||
],
|
||||
"options": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "entity",
|
||||
"description": "Manage entities.",
|
||||
"isGroup": true,
|
||||
"subcommands": [
|
||||
{
|
||||
"name": "list",
|
||||
"description": "List all entities of a given type.",
|
||||
"usage": "mem0 entity list <entity_type>",
|
||||
"needsBackend": true,
|
||||
"needsConfig": false,
|
||||
"resolveIds": false,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": false,
|
||||
"outputFormats": ["table", "json"],
|
||||
"defaultOutput": "table",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "entity_type",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"help": "Entity type: users, agents, apps, runs.",
|
||||
"choices": ["users", "agents", "apps", "runs"]
|
||||
}
|
||||
],
|
||||
"options": [
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: table, json.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "entities"
|
||||
},
|
||||
{
|
||||
"name": "delete",
|
||||
"description": "Delete an entity and ALL its memories (cascade).",
|
||||
"usage": "mem0 entity delete [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": false,
|
||||
"resolveIds": false,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": true,
|
||||
"outputFormats": ["text", "json", "quiet"],
|
||||
"defaultOutput": "text",
|
||||
"arguments": [],
|
||||
"options": [
|
||||
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "User ID.", "panel": "Scope" },
|
||||
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Agent ID.", "panel": "Scope" },
|
||||
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "App ID.", "panel": "Scope" },
|
||||
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Run ID.", "panel": "Scope" },
|
||||
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
|
||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "deleteEntities"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "init",
|
||||
"description": "Setup wizard for mem0 CLI. Supports Agent Mode bootstrap (--agent), email login (--email), or manual API key (--api-key).",
|
||||
"usage": "mem0 init [OPTIONS]",
|
||||
"needsBackend": false,
|
||||
"needsConfig": false,
|
||||
"resolveIds": false,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": false,
|
||||
"arguments": [],
|
||||
"options": [
|
||||
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." },
|
||||
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
|
||||
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
|
||||
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." },
|
||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." },
|
||||
{ "name": "agent", "flags": ["--agent"], "type": "boolean", "default": false, "help": "Bootstrap an unattended Agent Mode account (no email required)." },
|
||||
{ "name": "source", "flags": ["--source"], "type": "string", "default": null, "help": "Channel attribution for signup (e.g. github, hn, ph)." }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"description": "Check connectivity and authentication.",
|
||||
"usage": "mem0 status [OPTIONS]",
|
||||
"needsBackend": true,
|
||||
"needsConfig": true,
|
||||
"resolveIds": false,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": false,
|
||||
"arguments": [],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "help",
|
||||
"description": "Show help. Use --json for machine-readable output (for LLM agents).",
|
||||
"usage": "mem0 help [OPTIONS]",
|
||||
"needsBackend": false,
|
||||
"needsConfig": false,
|
||||
"resolveIds": false,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": false,
|
||||
"arguments": [],
|
||||
"options": [
|
||||
{ "name": "json", "flags": ["--json"], "type": "boolean", "default": false, "help": "Output machine-readable JSON for LLM agents." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
# mem0 CLI (Node.js)
|
||||
|
||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. TypeScript implementation.
|
||||
|
||||
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js **18+**
|
||||
- pnpm (`npm install -g pnpm`) — for development only
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -g @mem0/cli
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Interactive setup wizard
|
||||
mem0 init
|
||||
|
||||
# Or login via email
|
||||
mem0 init --email alice@company.com
|
||||
|
||||
# Or authenticate with an existing API key
|
||||
mem0 init --api-key m0-xxx
|
||||
|
||||
# Add a memory
|
||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||
|
||||
# Search memories
|
||||
mem0 search "What are Alice's preferences?" --user-id alice
|
||||
|
||||
# List all memories for a user
|
||||
mem0 list --user-id alice
|
||||
|
||||
# Get a specific memory
|
||||
mem0 get <memory-id>
|
||||
|
||||
# Update a memory
|
||||
mem0 update <memory-id> "I switched to light mode"
|
||||
|
||||
# Delete a memory
|
||||
mem0 delete <memory-id>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `mem0 init`
|
||||
|
||||
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||
|
||||
```bash
|
||||
mem0 init
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
mem0 init --email alice@company.com
|
||||
```
|
||||
|
||||
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||
|
||||
```bash
|
||||
mem0 init --api-key m0-xxx --user-id alice --force
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--api-key` | API key (skip prompt) |
|
||||
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||
| `--email` | Login via email verification code |
|
||||
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||
| `--force` | Overwrite existing config without confirmation |
|
||||
|
||||
### `mem0 add`
|
||||
|
||||
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||
|
||||
```bash
|
||||
mem0 add "I prefer dark mode" --user-id alice
|
||||
mem0 add --file conversation.json --user-id alice
|
||||
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Scope to a user |
|
||||
| `--agent-id` | Scope to an agent |
|
||||
| `--messages` | Conversation messages as JSON |
|
||||
| `-f, --file` | Read messages from a JSON file |
|
||||
| `-m, --metadata` | Custom metadata as JSON |
|
||||
| `--categories` | Categories (JSON array or comma-separated) |
|
||||
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||
|
||||
### `mem0 search`
|
||||
|
||||
Search memories using natural language.
|
||||
|
||||
```bash
|
||||
mem0 search "dietary restrictions" --user-id alice
|
||||
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `-k, --top-k` | Number of results (default: 10) |
|
||||
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||
| `--rerank` | Enable reranking |
|
||||
| `--keyword` | Use keyword search instead of semantic |
|
||||
| `--filter` | Advanced filter expression (JSON) |
|
||||
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 list`
|
||||
|
||||
List memories with optional filters and pagination.
|
||||
|
||||
```bash
|
||||
mem0 list --user-id alice
|
||||
mem0 list --user-id alice --category preferences --output json
|
||||
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `--page` | Page number (default: 1) |
|
||||
| `--page-size` | Results per page (default: 100) |
|
||||
| `--category` | Filter by category |
|
||||
| `--after` | Created after date (YYYY-MM-DD) |
|
||||
| `--before` | Created before date (YYYY-MM-DD) |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 get`
|
||||
|
||||
Retrieve a specific memory by ID.
|
||||
|
||||
```bash
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||
```
|
||||
|
||||
### `mem0 update`
|
||||
|
||||
Update the text or metadata of an existing memory.
|
||||
|
||||
```bash
|
||||
mem0 update <memory-id> "Updated preference text"
|
||||
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||
echo "new text" | mem0 update <memory-id>
|
||||
```
|
||||
|
||||
### `mem0 delete`
|
||||
|
||||
Delete a single memory, all memories for a scope, or an entire entity.
|
||||
|
||||
```bash
|
||||
# Delete a single memory
|
||||
mem0 delete <memory-id>
|
||||
|
||||
# Delete all memories for a user
|
||||
mem0 delete --all --user-id alice --force
|
||||
|
||||
# Delete all memories project-wide
|
||||
mem0 delete --all --project --force
|
||||
|
||||
# Preview what would be deleted
|
||||
mem0 delete --all --user-id alice --dry-run
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--all` | Delete all memories matching scope filters |
|
||||
| `--entity` | Delete the entity and all its memories |
|
||||
| `--project` | With `--all`: delete all memories project-wide |
|
||||
| `--dry-run` | Preview without deleting |
|
||||
| `--force` | Skip confirmation prompt |
|
||||
|
||||
### `mem0 import`
|
||||
|
||||
Bulk import memories from a JSON file.
|
||||
|
||||
```bash
|
||||
mem0 import data.json --user-id alice
|
||||
```
|
||||
|
||||
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||
|
||||
### `mem0 config`
|
||||
|
||||
View or modify the local CLI configuration.
|
||||
|
||||
```bash
|
||||
mem0 config show # Display current config (secrets redacted)
|
||||
mem0 config get api_key # Get a specific value
|
||||
mem0 config set user_id bob # Set a value
|
||||
```
|
||||
|
||||
### `mem0 entity`
|
||||
|
||||
List or delete entities (users, agents, apps, runs).
|
||||
|
||||
```bash
|
||||
mem0 entity list users
|
||||
mem0 entity list agents --output json
|
||||
mem0 entity delete --user-id alice --force
|
||||
```
|
||||
|
||||
### `mem0 event`
|
||||
|
||||
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||
|
||||
```bash
|
||||
# List recent events
|
||||
mem0 event list
|
||||
|
||||
# Check the status of a specific event
|
||||
mem0 event status <event-id>
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-o, --output` | Output format: `text`, `json` |
|
||||
|
||||
### `mem0 status`
|
||||
|
||||
Verify your API connection and display the current project.
|
||||
|
||||
```bash
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
|
||||
```bash
|
||||
mem0 --agent search "user preferences" --user-id alice
|
||||
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||
mem0 --agent list --user-id alice
|
||||
mem0 --agent delete --all --user-id alice --force
|
||||
```
|
||||
|
||||
Every command returns the same envelope shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "search",
|
||||
"duration_ms": 134,
|
||||
"scope": { "user_id": "alice" },
|
||||
"count": 2,
|
||||
"data": [
|
||||
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
What agent mode does differently from `--output json`:
|
||||
|
||||
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||
|
||||
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||
|
||||
## Output formats
|
||||
|
||||
Control how results are displayed with `--output`:
|
||||
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `text` | Human-readable with colors and formatting (default) |
|
||||
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||
| `table` | Tabular format (default for `list`) |
|
||||
| `quiet` | Minimal — just IDs or status codes |
|
||||
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||
|
||||
## Global flags
|
||||
|
||||
These flags are available on all commands:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||
| `--agent` | Alias for `--json` |
|
||||
| `--api-key` | Override the configured API key for this request |
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||
| `MEM0_BASE_URL` | API base URL |
|
||||
| `MEM0_USER_ID` | Default user ID |
|
||||
| `MEM0_AGENT_ID` | Default agent ID |
|
||||
| `MEM0_APP_ID` | Default app ID |
|
||||
| `MEM0_RUN_ID` | Default run ID |
|
||||
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||
|
||||
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd cli/node
|
||||
pnpm install
|
||||
|
||||
# Development mode (runs TypeScript directly, no build needed)
|
||||
pnpm dev --help
|
||||
pnpm dev add "test memory" --user-id alice
|
||||
pnpm dev search "test" --user-id alice
|
||||
|
||||
# Or build first, then run the compiled JS
|
||||
pnpm build
|
||||
node dist/index.js --help
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,91 @@
|
||||
# Development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js **18+**
|
||||
- pnpm (`npm install -g pnpm`)
|
||||
|
||||
## Setup
|
||||
|
||||
From the `node/` directory:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Running the CLI
|
||||
|
||||
There are two ways to run the CLI during development:
|
||||
|
||||
### Option 1: Development mode (no build needed)
|
||||
|
||||
Uses `tsx` to run TypeScript directly. Pass CLI arguments after `pnpm dev`:
|
||||
|
||||
```bash
|
||||
pnpm dev --help
|
||||
pnpm dev version
|
||||
pnpm dev add "test memory" --user-id alice
|
||||
pnpm dev search "test" --user-id alice
|
||||
pnpm dev config show
|
||||
```
|
||||
|
||||
> **Note:** Do NOT use `pnpm dev -- --help`. With pnpm, arguments pass through directly — adding `--` inserts a literal `--` that breaks the CLI parser.
|
||||
|
||||
### Option 2: Build and run compiled JS
|
||||
|
||||
```bash
|
||||
# Build first
|
||||
pnpm build
|
||||
|
||||
# Run the compiled CLI
|
||||
node dist/index.js --help
|
||||
node dist/index.js version
|
||||
node dist/index.js add "test memory" --user-id alice
|
||||
```
|
||||
|
||||
### Option 3: Link globally (makes `mem0` available system-wide)
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm link --global
|
||||
|
||||
# Now use it like a normal CLI
|
||||
mem0 --help
|
||||
mem0 version
|
||||
```
|
||||
|
||||
> **Warning:** If you also have the Python CLI installed, both register the `mem0` command. The last one linked/installed wins. Unlink with `pnpm unlink --global`.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
The compiled output is in `dist/`.
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Watch mode
|
||||
pnpm test:watch
|
||||
```
|
||||
|
||||
## Lint
|
||||
|
||||
```bash
|
||||
# Check
|
||||
pnpm lint
|
||||
|
||||
# Auto-fix
|
||||
pnpm lint:fix
|
||||
```
|
||||
|
||||
## Type checking
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.5",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mem0": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsx src/index.ts",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "biome check src/",
|
||||
"lint:fix": "biome check --write src/",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "mem0.ai <founders@mem0.ai>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mem0ai/mem0",
|
||||
"directory": "cli/node"
|
||||
},
|
||||
"keywords": ["mem0", "memory", "ai", "agents", "cli"],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-table3": "^0.6.4",
|
||||
"ora": "^8.0.0",
|
||||
"boxen": "^7.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.0",
|
||||
"tsup": "^8.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"vitest": "^1.5.0",
|
||||
"@biomejs/biome": "^1.7.0",
|
||||
"@types/node": "^20.0.0"
|
||||
}
|
||||
}
|
||||
Generated
+2066
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Detect whether the CLI is being invoked from inside an AI-agent context.
|
||||
*
|
||||
* Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
|
||||
* agent runtime env var is present. The return value is a context **trigger
|
||||
* only** — the canonical agent identity is self-declared by the agent via
|
||||
* `--agent-caller <name>` (Proof Editor-style) and never sniffed from env
|
||||
* vars to fill the `agent_caller` field on the APIKey row.
|
||||
*
|
||||
* Returns a short name or null. Honest reporting depends on `--agent-caller`;
|
||||
* this list is just enough to enable the zero-friction auto-bootstrap UX.
|
||||
*/
|
||||
|
||||
const AGENT_CALLER_ENV: ReadonlyArray<readonly [string, readonly string[]]> = [
|
||||
["claude-code", ["CLAUDECODE", "CLAUDE_CODE"]],
|
||||
["cursor", ["CURSOR_AGENT", "CURSOR_SESSION_ID"]],
|
||||
["codex", ["CODEX_CLI", "OPENAI_CODEX"]],
|
||||
["cline", ["CLINE_AGENT", "CLINE"]],
|
||||
["continue", ["CONTINUE_AGENT", "CONTINUE_SESSION"]],
|
||||
["aider", ["AIDER_SESSION"]],
|
||||
["goose", ["GOOSE_AGENT"]],
|
||||
["windsurf", ["WINDSURF_AGENT"]],
|
||||
] as const;
|
||||
|
||||
export function detectAgentCaller(): string | null {
|
||||
for (const [name, envVars] of AGENT_CALLER_ENV) {
|
||||
if (envVars.some((v) => process.env[v])) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Abstract backend interface and factory.
|
||||
*/
|
||||
|
||||
import type { Mem0Config } from "../config.js";
|
||||
import { PlatformBackend } from "./platform.js";
|
||||
|
||||
export interface AddOptions {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
immutable?: boolean;
|
||||
infer?: boolean;
|
||||
expires?: string;
|
||||
categories?: string[];
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
topK?: number;
|
||||
threshold?: number;
|
||||
rerank?: boolean;
|
||||
keyword?: boolean;
|
||||
filters?: Record<string, unknown>;
|
||||
fields?: string[];
|
||||
}
|
||||
|
||||
export interface ListOptions {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
category?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
}
|
||||
|
||||
export interface DeleteOptions {
|
||||
all?: boolean;
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
}
|
||||
|
||||
export interface EntityIds {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
}
|
||||
|
||||
export interface Backend {
|
||||
add(
|
||||
content?: string,
|
||||
messages?: Record<string, unknown>[],
|
||||
opts?: AddOptions,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
search(
|
||||
query: string,
|
||||
opts?: SearchOptions,
|
||||
): Promise<Record<string, unknown>[]>;
|
||||
|
||||
get(memoryId: string): Promise<Record<string, unknown>>;
|
||||
|
||||
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
|
||||
|
||||
update(
|
||||
memoryId: string,
|
||||
content?: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
delete(
|
||||
memoryId?: string,
|
||||
opts?: DeleteOptions,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
||||
|
||||
ping(): Promise<Record<string, unknown>>;
|
||||
|
||||
status(opts?: { userId?: string; agentId?: string }): Promise<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
|
||||
entities(entityType: string): Promise<Record<string, unknown>[]>;
|
||||
|
||||
listEvents(): Promise<Record<string, unknown>[]>;
|
||||
|
||||
getEvent(eventId: string): Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export class AuthError extends Error {
|
||||
constructor(
|
||||
message = "Authentication failed. Your API key may be invalid or expired.",
|
||||
) {
|
||||
super(message);
|
||||
this.name = "AuthError";
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Error {
|
||||
constructor(path: string) {
|
||||
super(`Resource not found: ${path}`);
|
||||
this.name = "NotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export class APIError extends Error {
|
||||
constructor(path: string, detail: string) {
|
||||
super(`Bad request to ${path}: ${detail}`);
|
||||
this.name = "APIError";
|
||||
}
|
||||
}
|
||||
|
||||
export function getBackend(config: Mem0Config): Backend {
|
||||
return new PlatformBackend(config.platform);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Backend factory re-export.
|
||||
*/
|
||||
|
||||
export { getBackend } from "./base.js";
|
||||
export type {
|
||||
Backend,
|
||||
AddOptions,
|
||||
SearchOptions,
|
||||
ListOptions,
|
||||
DeleteOptions,
|
||||
EntityIds,
|
||||
} from "./base.js";
|
||||
export { AuthError, NotFoundError, APIError } from "./base.js";
|
||||
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Platform (SaaS) backend — communicates with api.mem0.ai.
|
||||
*/
|
||||
|
||||
import type { PlatformConfig } from "../config.js";
|
||||
import { captureNotice, isAgentMode } from "../state.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
import {
|
||||
APIError,
|
||||
type AddOptions,
|
||||
AuthError,
|
||||
type Backend,
|
||||
type DeleteOptions,
|
||||
type EntityIds,
|
||||
type ListOptions,
|
||||
NotFoundError,
|
||||
type SearchOptions,
|
||||
} from "./base.js";
|
||||
|
||||
export class PlatformBackend implements Backend {
|
||||
private baseUrl: string;
|
||||
private headers: Record<string, string>;
|
||||
|
||||
constructor(config: PlatformConfig) {
|
||||
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||||
this.headers = {
|
||||
Authorization: `Token ${config.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
"X-Mem0-Client-Version": CLI_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
private async _request(
|
||||
method: string,
|
||||
path: string,
|
||||
opts?: { json?: unknown; params?: Record<string, string> },
|
||||
): Promise<unknown> {
|
||||
let url = `${this.baseUrl}${path}`;
|
||||
if (opts?.params) {
|
||||
const qs = new URLSearchParams(opts.params).toString();
|
||||
url += `?${qs}`;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...this.headers,
|
||||
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
|
||||
};
|
||||
|
||||
const fetchOpts: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
};
|
||||
if (opts?.json) {
|
||||
fetchOpts.body = JSON.stringify(opts.json);
|
||||
}
|
||||
|
||||
const resp = await fetch(url, fetchOpts);
|
||||
|
||||
if (resp.status === 401) {
|
||||
throw new AuthError();
|
||||
}
|
||||
if (resp.status === 404) {
|
||||
throw new NotFoundError(path);
|
||||
}
|
||||
if (resp.status === 400) {
|
||||
let detail: string;
|
||||
try {
|
||||
const body = (await resp.json()) as Record<string, unknown>;
|
||||
detail =
|
||||
((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
|
||||
resp.statusText;
|
||||
} catch {
|
||||
detail = resp.statusText;
|
||||
}
|
||||
throw new APIError(path, detail);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
let detail: string = resp.statusText;
|
||||
try {
|
||||
const body = (await resp.json()) as Record<string, unknown>;
|
||||
detail = (body.detail ?? body.message ?? resp.statusText) as string;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw new Error(`HTTP ${resp.status}: ${detail}`);
|
||||
}
|
||||
if (resp.status === 204) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
// Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
||||
// fallback for endpoints returning non-dict / non-dict-leading payloads)
|
||||
// and stash for end-of-command surfacing.
|
||||
let notice: string | null = null;
|
||||
if (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
!Array.isArray(data) &&
|
||||
"mem0_notice" in data
|
||||
) {
|
||||
notice = (data as Record<string, unknown>).mem0_notice as string;
|
||||
// biome-ignore lint/performance/noDelete: intentional strip so downstream consumers don't see duplicate notice
|
||||
delete (data as Record<string, unknown>).mem0_notice;
|
||||
} else if (
|
||||
Array.isArray(data) &&
|
||||
data.length > 0 &&
|
||||
typeof data[0] === "object" &&
|
||||
data[0] !== null &&
|
||||
"mem0_notice" in data[0]
|
||||
) {
|
||||
notice = (data[0] as Record<string, unknown>).mem0_notice as string;
|
||||
// biome-ignore lint/performance/noDelete: see above.
|
||||
delete (data[0] as Record<string, unknown>).mem0_notice;
|
||||
}
|
||||
if (!notice) {
|
||||
notice = resp.headers.get("X-Mem0-Notice-Message") ?? null;
|
||||
}
|
||||
captureNotice(notice);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async add(
|
||||
content?: string,
|
||||
messages?: Record<string, unknown>[],
|
||||
opts: AddOptions = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const payload: Record<string, unknown> = {};
|
||||
|
||||
if (messages) {
|
||||
payload.messages = messages;
|
||||
} else if (content) {
|
||||
payload.messages = [{ role: "user", content }];
|
||||
}
|
||||
|
||||
if (opts.userId) payload.user_id = opts.userId;
|
||||
if (opts.agentId) payload.agent_id = opts.agentId;
|
||||
if (opts.appId) payload.app_id = opts.appId;
|
||||
if (opts.runId) payload.run_id = opts.runId;
|
||||
if (opts.metadata) payload.metadata = opts.metadata;
|
||||
if (opts.immutable) payload.immutable = true;
|
||||
if (opts.infer === false) payload.infer = false;
|
||||
if (opts.expires) payload.expiration_date = opts.expires;
|
||||
if (opts.categories) payload.categories = opts.categories;
|
||||
payload.source = "CLI";
|
||||
|
||||
return (await this._request("POST", "/v3/memories/add/", {
|
||||
json: payload,
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
private _buildFilters(opts: {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
extraFilters?: Record<string, unknown>;
|
||||
}): Record<string, unknown> | undefined {
|
||||
// If caller passed a pre-built filter structure, use it directly
|
||||
if (
|
||||
opts.extraFilters &&
|
||||
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
|
||||
) {
|
||||
return opts.extraFilters;
|
||||
}
|
||||
|
||||
const andConditions: Record<string, unknown>[] = [];
|
||||
if (opts.userId) andConditions.push({ user_id: opts.userId });
|
||||
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
|
||||
if (opts.appId) andConditions.push({ app_id: opts.appId });
|
||||
if (opts.runId) andConditions.push({ run_id: opts.runId });
|
||||
|
||||
if (opts.extraFilters) {
|
||||
for (const [k, v] of Object.entries(opts.extraFilters)) {
|
||||
andConditions.push({ [k]: v });
|
||||
}
|
||||
}
|
||||
|
||||
if (andConditions.length === 1) return andConditions[0];
|
||||
if (andConditions.length > 1) return { AND: andConditions };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async search(
|
||||
query: string,
|
||||
opts: SearchOptions = {},
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
const payload: Record<string, unknown> = {
|
||||
query,
|
||||
top_k: opts.topK ?? 10,
|
||||
threshold: opts.threshold ?? 0.3,
|
||||
};
|
||||
|
||||
const apiFilters = this._buildFilters({
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
appId: opts.appId,
|
||||
runId: opts.runId,
|
||||
extraFilters: opts.filters,
|
||||
});
|
||||
if (apiFilters) payload.filters = apiFilters;
|
||||
if (opts.rerank) payload.rerank = true;
|
||||
if (opts.keyword) payload.keyword_search = true;
|
||||
if (opts.fields) payload.fields = opts.fields;
|
||||
payload.source = "CLI";
|
||||
|
||||
const result = (await this._request("POST", "/v3/memories/search/", {
|
||||
json: payload,
|
||||
})) as unknown;
|
||||
if (Array.isArray(result)) return result;
|
||||
const obj = result as Record<string, unknown>;
|
||||
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
|
||||
params: { source: "CLI" },
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async listMemories(
|
||||
opts: ListOptions = {},
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
const payload: Record<string, unknown> = {};
|
||||
const params: Record<string, string> = {
|
||||
page: String(opts.page ?? 1),
|
||||
page_size: String(opts.pageSize ?? 100),
|
||||
};
|
||||
|
||||
const extra: Record<string, unknown> = {};
|
||||
if (opts.category) {
|
||||
extra.categories = { contains: opts.category };
|
||||
}
|
||||
if (opts.after) {
|
||||
extra.created_at = {
|
||||
...(extra.created_at as Record<string, unknown> | undefined),
|
||||
gte: opts.after,
|
||||
};
|
||||
}
|
||||
if (opts.before) {
|
||||
extra.created_at = {
|
||||
...(extra.created_at as Record<string, unknown> | undefined),
|
||||
lte: opts.before,
|
||||
};
|
||||
}
|
||||
|
||||
const apiFilters = this._buildFilters({
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
appId: opts.appId,
|
||||
runId: opts.runId,
|
||||
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
|
||||
});
|
||||
if (apiFilters) payload.filters = apiFilters;
|
||||
payload.source = "CLI";
|
||||
|
||||
const result = (await this._request("POST", "/v3/memories/", {
|
||||
json: payload,
|
||||
params,
|
||||
})) as unknown;
|
||||
if (Array.isArray(result)) return result;
|
||||
const obj = result as Record<string, unknown>;
|
||||
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
async update(
|
||||
memoryId: string,
|
||||
content?: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (content) payload.text = content;
|
||||
if (metadata) payload.metadata = metadata;
|
||||
payload.source = "CLI";
|
||||
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
|
||||
json: payload,
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async delete(
|
||||
memoryId?: string,
|
||||
opts: DeleteOptions = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (opts.all) {
|
||||
const params: Record<string, string> = { source: "CLI" };
|
||||
if (opts.userId) params.user_id = opts.userId;
|
||||
if (opts.agentId) params.agent_id = opts.agentId;
|
||||
if (opts.appId) params.app_id = opts.appId;
|
||||
if (opts.runId) params.run_id = opts.runId;
|
||||
return (await this._request("DELETE", "/v1/memories/", {
|
||||
params,
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
if (memoryId) {
|
||||
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
|
||||
params: { source: "CLI" },
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
throw new Error("Either memoryId or --all is required");
|
||||
}
|
||||
|
||||
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
|
||||
// v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||
const typeMap: [string, string | undefined][] = [
|
||||
["user", opts.userId],
|
||||
["agent", opts.agentId],
|
||||
["app", opts.appId],
|
||||
["run", opts.runId],
|
||||
];
|
||||
const entities = typeMap.filter(([, v]) => v) as [string, string][];
|
||||
if (entities.length === 0) {
|
||||
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||
}
|
||||
// Delete each provided entity via the v2 path-based endpoint
|
||||
let result: Record<string, unknown> = {};
|
||||
for (const [entityType, entityId] of entities) {
|
||||
result = (await this._request(
|
||||
"DELETE",
|
||||
`/v2/entities/${entityType}/${entityId}/`,
|
||||
{ params: { source: "CLI" } },
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async ping(): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async status(
|
||||
opts: { userId?: string; agentId?: string } = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
await this.ping();
|
||||
return { connected: true, backend: "platform", base_url: this.baseUrl };
|
||||
} catch (e) {
|
||||
return {
|
||||
connected: false,
|
||||
backend: "platform",
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async entities(entityType: string): Promise<Record<string, unknown>[]> {
|
||||
const result = (await this._request("GET", "/v1/entities/")) as unknown;
|
||||
let items: Record<string, unknown>[];
|
||||
if (Array.isArray(result)) {
|
||||
items = result;
|
||||
} else {
|
||||
items = ((result as Record<string, unknown>).results ?? []) as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
}
|
||||
|
||||
const typeMap: Record<string, string> = {
|
||||
users: "user",
|
||||
agents: "agent",
|
||||
apps: "app",
|
||||
runs: "run",
|
||||
};
|
||||
const targetType = typeMap[entityType];
|
||||
if (targetType) {
|
||||
items = items.filter(
|
||||
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async listEvents(): Promise<Record<string, unknown>[]> {
|
||||
const result = (await this._request("GET", "/v1/events/")) as unknown;
|
||||
if (Array.isArray(result)) return result;
|
||||
return ((result as Record<string, unknown>).results ?? []) as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
}
|
||||
|
||||
async getEvent(eventId: string): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Branding and ASCII art for mem0 CLI.
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import ora, { type Ora } from "ora";
|
||||
import { getCurrentCommand, isAgentMode } from "./state.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
export const LOGO = `
|
||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
||||
████╗ ████║██╔════╝████╗ ████║██╔═████╗ ██╔════╝██║ ██║
|
||||
██╔████╔██║█████╗ ██╔████╔██║██║██╔██║ ██║ ██║ ██║
|
||||
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║████╔╝██║ ██║ ██║ ██║
|
||||
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚██████╗███████╗██║
|
||||
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝
|
||||
`;
|
||||
|
||||
export const LOGO_MINI = "◆ mem0";
|
||||
export const TAGLINE = "The Memory Layer for AI Agents";
|
||||
|
||||
export const BRAND_COLOR = "#8b5cf6";
|
||||
export const ACCENT_COLOR = "#a78bfa";
|
||||
export const SUCCESS_COLOR = "#22c55e";
|
||||
export const ERROR_COLOR = "#ef4444";
|
||||
export const WARNING_COLOR = "#f59e0b";
|
||||
export const DIM_COLOR = "#6b7280";
|
||||
|
||||
const brand = chalk.hex(BRAND_COLOR);
|
||||
const accent = chalk.hex(ACCENT_COLOR);
|
||||
const success = chalk.hex(SUCCESS_COLOR);
|
||||
const error = chalk.hex(ERROR_COLOR);
|
||||
const warning = chalk.hex(WARNING_COLOR);
|
||||
const dim = chalk.hex(DIM_COLOR);
|
||||
|
||||
/**
|
||||
* Choose a symbol based on TTY/NO_COLOR. Fancy for interactive terminals,
|
||||
* plain-text for piped/non-TTY or NO_COLOR environments.
|
||||
*/
|
||||
export function sym(fancy: string, plain: string): string {
|
||||
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
|
||||
return fancy;
|
||||
}
|
||||
|
||||
export function printBanner(): void {
|
||||
if (isAgentMode()) return;
|
||||
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
||||
const logoLines = LOGO.trimEnd().split("\n");
|
||||
const tagline = ` ${TAGLINE}`;
|
||||
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
|
||||
const contentLines = ["", ...logoLines, "", tagline, ""];
|
||||
|
||||
// Compute inner width from longest content line + padding both sides
|
||||
const maxContent = Math.max(...contentLines.map((l) => l.length));
|
||||
const innerWidth = maxContent + pad * 2;
|
||||
const totalWidth = innerWidth + 2; // + 2 for │ borders
|
||||
|
||||
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
|
||||
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
|
||||
const bottomBorder = brand(
|
||||
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
|
||||
);
|
||||
|
||||
const body = contentLines.map((line) => {
|
||||
const rightPad = innerWidth - pad - line.length;
|
||||
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
|
||||
});
|
||||
// Re-color tagline line with accent instead of brand.bold
|
||||
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
|
||||
const taglineRightPad = innerWidth - pad - tagline.length;
|
||||
body[taglineIdx] =
|
||||
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
|
||||
|
||||
console.log(topBorder);
|
||||
for (const line of body) console.log(line);
|
||||
console.log(bottomBorder);
|
||||
}
|
||||
|
||||
export function printSuccess(message: string): void {
|
||||
if (isAgentMode()) return;
|
||||
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
|
||||
}
|
||||
|
||||
export function printError(message: string, hint?: string): void {
|
||||
if (isAgentMode()) {
|
||||
const envelope = {
|
||||
status: "error",
|
||||
command: getCurrentCommand(),
|
||||
error: message,
|
||||
data: null,
|
||||
};
|
||||
console.log(JSON.stringify(envelope));
|
||||
return;
|
||||
}
|
||||
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
|
||||
const resolvedHint =
|
||||
hint ??
|
||||
(message.includes("Authentication failed")
|
||||
? `Run ${brand("mem0 init")} to reconfigure your API key · https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-node`
|
||||
: undefined);
|
||||
if (resolvedHint) {
|
||||
console.error(` ${dim(resolvedHint)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function printWarning(message: string): void {
|
||||
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
|
||||
}
|
||||
|
||||
export function printInfo(message: string): void {
|
||||
if (isAgentMode()) return;
|
||||
console.error(`${brand(sym("◆", "*"))} ${message}`);
|
||||
}
|
||||
|
||||
export function printScope(ids: Record<string, string | undefined>): void {
|
||||
if (isAgentMode()) return;
|
||||
const parts: string[] = [];
|
||||
for (const [key, val] of Object.entries(ids)) {
|
||||
if (val) {
|
||||
parts.push(`${key}=${val}`);
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface TimedStatusContext {
|
||||
successMsg: string;
|
||||
errorMsg: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run an async function with a spinner, timing the operation.
|
||||
* Equivalent to Python's timed_status context manager.
|
||||
*/
|
||||
export async function timedStatus<T>(
|
||||
message: string,
|
||||
fn: (ctx: TimedStatusContext) => Promise<T>,
|
||||
): Promise<T> {
|
||||
if (isAgentMode()) {
|
||||
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||
return fn(ctx);
|
||||
}
|
||||
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||
const spinner = ora({
|
||||
text: dim(message),
|
||||
color: "yellow",
|
||||
stream: process.stderr,
|
||||
}).start();
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
const result = await fn(ctx);
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||
spinner.stop();
|
||||
if (ctx.successMsg) {
|
||||
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||
spinner.stop();
|
||||
if (ctx.errorMsg) {
|
||||
printError(`${ctx.errorMsg} (${elapsed}s)`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** Format helpers using brand colors for external use. */
|
||||
export const colors = { brand, accent, success, error, warning, dim };
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Agent Mode commands — bootstrap (unattended signup) and OTP-based claim.
|
||||
*/
|
||||
|
||||
import readline from "node:readline";
|
||||
import { colors, printError, printInfo, printSuccess } from "../branding.js";
|
||||
import { type Mem0Config, saveConfig } from "../config.js";
|
||||
|
||||
const { brand, dim } = colors;
|
||||
|
||||
const SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
} as const;
|
||||
|
||||
export interface BootstrapEnvelope {
|
||||
api_key: string;
|
||||
default_user_id: string;
|
||||
org_id: string;
|
||||
project_id: string;
|
||||
mcp_url?: string;
|
||||
smoke_test_url?: string;
|
||||
claim_command?: string;
|
||||
mem0_notice?: string;
|
||||
}
|
||||
|
||||
function isValidEnvelope(v: unknown): v is BootstrapEnvelope {
|
||||
return (
|
||||
!!v &&
|
||||
typeof v === "object" &&
|
||||
typeof (v as BootstrapEnvelope).api_key === "string" &&
|
||||
(v as BootstrapEnvelope).api_key.length > 0 &&
|
||||
typeof (v as BootstrapEnvelope).default_user_id === "string" &&
|
||||
(v as BootstrapEnvelope).default_user_id.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/agent_mode/ and mutate config in place.
|
||||
*
|
||||
* @param config - Mem0Config mutated in place with the new platform values.
|
||||
* @param source - `--source` flag passthrough (analytics tag, free-form).
|
||||
* @param agentCaller - Self-declared agent identity passed via `--agent-caller`
|
||||
* (e.g. `claude-code`, `cursor`). May be null when the caller omitted the
|
||||
* flag; the agent can backfill later via `mem0 identify <name>`. Sent to the
|
||||
* backend in the request body and saved into `platform.agentCaller` for
|
||||
* local introspection.
|
||||
*/
|
||||
export async function bootstrapViaBackend(
|
||||
config: Mem0Config,
|
||||
{
|
||||
source,
|
||||
agentCaller,
|
||||
}: { source?: string | null; agentCaller?: string | null } = {},
|
||||
): Promise<void> {
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
const body: Record<string, unknown> = {};
|
||||
if (source) body.source = source;
|
||||
if (agentCaller) body.agent_caller = agentCaller;
|
||||
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...SOURCE_HEADERS,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
} catch (err) {
|
||||
printError(
|
||||
`Network error contacting Mem0: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (resp.status === 429) {
|
||||
printError("Rate-limited. Try again in a few minutes.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (resp.status === 503) {
|
||||
printError("Agent Mode is temporarily disabled. Try again later.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
let detail: string = resp.statusText;
|
||||
try {
|
||||
const errBody = (await resp.json()) as {
|
||||
error?: string;
|
||||
detail?: string;
|
||||
};
|
||||
detail = errBody.error ?? errBody.detail ?? resp.statusText;
|
||||
} catch {
|
||||
/* leave detail as statusText */
|
||||
}
|
||||
// Backend's @ratelimit decorator raises PermissionDenied, which DRF
|
||||
// translates to a generic 403 "You do not have permission to perform
|
||||
// this action." That's opaque — surface it as the rate-limit message
|
||||
// it actually is.
|
||||
if (resp.status === 403 && /permission/i.test(detail)) {
|
||||
printError(
|
||||
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
printError(`Bootstrap failed: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const envelope = (await resp.json()) as BootstrapEnvelope;
|
||||
if (!isValidEnvelope(envelope)) {
|
||||
// Defend against partial/malformed backend responses (e.g. {api_key: null}).
|
||||
// Without this guard, the typed `string` field is silently set to
|
||||
// undefined/null and persisted, producing confusing downstream errors.
|
||||
printError(
|
||||
"Bootstrap response missing required fields — please update the CLI.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.platform.apiKey = envelope.api_key;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.agentMode = true;
|
||||
config.platform.createdVia = "agent_mode";
|
||||
config.platform.agentCaller = agentCaller ?? "";
|
||||
config.platform.claimedAt = "";
|
||||
config.platform.defaultUserId = envelope.default_user_id;
|
||||
// Adopt the slug-derived user_id as the default scope for memory ops.
|
||||
config.defaults.userId = envelope.default_user_id;
|
||||
saveConfig(config);
|
||||
|
||||
printSuccess(
|
||||
`Agent Mode active. Default user_id: ${envelope.default_user_id}`,
|
||||
);
|
||||
if (envelope.mem0_notice) {
|
||||
console.log(`\n\x1b[33m🔔 ${envelope.mem0_notice}\x1b[0m\n`);
|
||||
} else {
|
||||
// Fallback for older backends without the unified notice field.
|
||||
const claimCmd = envelope.claim_command ?? "mem0 init --email <your-email>";
|
||||
console.log(` ${dim(`To claim this account later: ${claimCmd}`)}`);
|
||||
}
|
||||
|
||||
if (!agentCaller) {
|
||||
console.log(
|
||||
` ${dim("If you are an AI agent, identify yourself: `mem0 identify <your-name>` (e.g. claude-code, cursor).")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim an existing Agent Mode account via OTP — no browser, no polling.
|
||||
*
|
||||
* Hits /api/v1/auth/email_code/ to send a verification code, prompts for it
|
||||
* interactively (or accepts via `code`), then sends it to /verify/ alongside
|
||||
* `agent_mode_api_key`. Backend's verify_email_code runs upgrade-in-place
|
||||
* inline and returns the claim result.
|
||||
*/
|
||||
export async function claimViaOtp(
|
||||
config: Mem0Config,
|
||||
{ email, code }: { email: string; code?: string },
|
||||
): Promise<void> {
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
if (!config.platform.apiKey || !config.platform.agentMode) {
|
||||
printError(
|
||||
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rawKey = config.platform.apiKey;
|
||||
|
||||
// Step 1: request OTP (unless --code was supplied)
|
||||
if (!code) {
|
||||
const sendResp = await fetch(`${baseUrl}/api/v1/auth/email_code/`, {
|
||||
method: "POST",
|
||||
headers: { ...SOURCE_HEADERS, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (sendResp.status === 429) {
|
||||
printError("Too many attempts. Try again in a few minutes.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!sendResp.ok) {
|
||||
let detail: string = sendResp.statusText;
|
||||
try {
|
||||
const errBody = (await sendResp.json()) as { error?: string };
|
||||
if (errBody.error) detail = errBody.error;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Failed to send code: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
printSuccess(`Verification code sent to ${email}. Check your inbox.`);
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
printError(
|
||||
"No --code provided and terminal is non-interactive.",
|
||||
`Re-run: mem0 init --email ${email} --code <code>`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log();
|
||||
code = await promptLine(` ${brand("Verification Code")}`);
|
||||
if (!code) {
|
||||
printError("Code is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: verify + claim atomically
|
||||
const verifyResp = await fetch(`${baseUrl}/api/v1/auth/email_code/verify/`, {
|
||||
method: "POST",
|
||||
headers: { ...SOURCE_HEADERS, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
code: code.trim(),
|
||||
agent_mode_api_key: rawKey,
|
||||
}),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
|
||||
if (!verifyResp.ok) {
|
||||
let detail: string = verifyResp.statusText;
|
||||
let errCode = "";
|
||||
try {
|
||||
const errBody = (await verifyResp.json()) as {
|
||||
error?: string;
|
||||
code?: string;
|
||||
};
|
||||
if (errBody.error) detail = errBody.error;
|
||||
if (errBody.code) errCode = errBody.code;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Claim failed: ${detail}`);
|
||||
if (errCode === "email_already_claimed") {
|
||||
console.log(
|
||||
` ${dim("Tip: this email already has a Mem0 account. Sign in there and run `mem0 link <key>` to attach this agent.")}`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const claimBody = (await verifyResp.json()) as {
|
||||
claimed?: boolean;
|
||||
claimed_at?: string;
|
||||
};
|
||||
if (!claimBody.claimed) {
|
||||
printError(`Unexpected verify response: ${JSON.stringify(claimBody)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.platform.agentMode = false;
|
||||
config.platform.claimedAt = claimBody.claimed_at ?? new Date().toISOString();
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
saveConfig(config);
|
||||
|
||||
printSuccess(`Agent claimed to ${email}. Your API key is unchanged.`);
|
||||
}
|
||||
|
||||
function promptLine(label: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${label}: `, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Config management commands: show, set, get.
|
||||
*/
|
||||
|
||||
import Table from "cli-table3";
|
||||
import { colors, printError, printSuccess } from "../branding.js";
|
||||
import {
|
||||
getNestedValue,
|
||||
loadConfig,
|
||||
redactKey,
|
||||
saveConfig,
|
||||
setNestedValue,
|
||||
} from "../config.js";
|
||||
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||
|
||||
const { brand, accent, dim } = colors;
|
||||
|
||||
export function cmdConfigShow(opts: { output?: string } = {}): void {
|
||||
setCurrentCommand("config show");
|
||||
const config = loadConfig();
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "config show",
|
||||
data: {
|
||||
defaults: {
|
||||
user_id: config.defaults.userId || null,
|
||||
agent_id: config.defaults.agentId || null,
|
||||
app_id: config.defaults.appId || null,
|
||||
run_id: config.defaults.runId || null,
|
||||
},
|
||||
platform: {
|
||||
api_key: redactKey(config.platform.apiKey),
|
||||
base_url: config.platform.baseUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(` ${brand("◆ mem0 Configuration")}\n`);
|
||||
|
||||
const table = new Table({
|
||||
head: [accent("Key"), accent("Value")],
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
// Defaults
|
||||
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
|
||||
table.push([
|
||||
"defaults.agent_id",
|
||||
config.defaults.agentId || dim("(not set)"),
|
||||
]);
|
||||
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
|
||||
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
|
||||
table.push(["", ""]);
|
||||
|
||||
// Platform
|
||||
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
|
||||
table.push(["platform.base_url", config.platform.baseUrl]);
|
||||
|
||||
console.log(table.toString());
|
||||
console.log();
|
||||
}
|
||||
|
||||
export function cmdConfigGet(key: string): void {
|
||||
setCurrentCommand("config get");
|
||||
const config = loadConfig();
|
||||
const value = getNestedValue(config, key);
|
||||
|
||||
if (value === undefined) {
|
||||
printError(`Unknown config key: ${key}`);
|
||||
} else {
|
||||
// Redact secrets
|
||||
const displayValue =
|
||||
key.includes("api_key") || key.split(".").pop() === "key"
|
||||
? redactKey(String(value))
|
||||
: String(value);
|
||||
if (isAgentMode()) {
|
||||
formatAgentEnvelope({
|
||||
command: "config get",
|
||||
data: { key, value: displayValue },
|
||||
});
|
||||
} else {
|
||||
console.log(displayValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cmdConfigSet(key: string, value: string): void {
|
||||
setCurrentCommand("config set");
|
||||
const config = loadConfig();
|
||||
if (setNestedValue(config, key, value)) {
|
||||
saveConfig(config);
|
||||
const display = key.includes("key") ? redactKey(value) : value;
|
||||
if (isAgentMode()) {
|
||||
formatAgentEnvelope({
|
||||
command: "config set",
|
||||
data: { key, value: display },
|
||||
});
|
||||
} else {
|
||||
printSuccess(`${key} = ${display}`);
|
||||
}
|
||||
} else {
|
||||
printError(`Unknown config key: ${key}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Entity management commands.
|
||||
*/
|
||||
|
||||
import readline from "node:readline";
|
||||
import Table from "cli-table3";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import {
|
||||
colors,
|
||||
printError,
|
||||
printInfo,
|
||||
printSuccess,
|
||||
timedStatus,
|
||||
} from "../branding.js";
|
||||
import { formatAgentEnvelope, formatJson } from "../output.js";
|
||||
import { setCurrentCommand } from "../state.js";
|
||||
|
||||
const { brand, accent, dim } = colors;
|
||||
|
||||
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
|
||||
|
||||
export async function cmdEntitiesList(
|
||||
backend: Backend,
|
||||
entityType: string,
|
||||
opts: { output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("entity list");
|
||||
if (!VALID_TYPES.has(entityType)) {
|
||||
printError(
|
||||
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let results: Record<string, unknown>[];
|
||||
try {
|
||||
results = await timedStatus(`Fetching ${entityType}...`, async () => {
|
||||
return backend.entities(entityType);
|
||||
});
|
||||
} catch (e) {
|
||||
printError(
|
||||
e instanceof Error ? e.message : String(e),
|
||||
"This feature may require the mem0 Platform.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "entity list",
|
||||
data: results,
|
||||
count: results.length,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!results.length) {
|
||||
printInfo(`No ${entityType} found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: [accent("Name / ID"), accent("Created")],
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const entity of results) {
|
||||
const name = String(entity.name ?? entity.id ?? "—");
|
||||
const created = String(entity.created_at ?? "—").slice(0, 10);
|
||||
table.push([name, created]);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(table.toString());
|
||||
console.log(
|
||||
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
export async function cmdEntitiesDelete(
|
||||
backend: Backend,
|
||||
opts: {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
dryRun?: boolean;
|
||||
force: boolean;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("entity delete");
|
||||
const { isAgentMode } = await import("../state.js");
|
||||
if (isAgentMode() && !opts.force) {
|
||||
printError("Destructive operation requires --force in agent mode.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
||||
printError(
|
||||
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scopeParts: string[] = [];
|
||||
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||
const scope = scopeParts.join(", ");
|
||||
|
||||
if (opts.dryRun) {
|
||||
printInfo(`Would delete entity ${scope} and all its memories.`);
|
||||
printInfo("No changes made.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.force) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
||||
resolve,
|
||||
);
|
||||
});
|
||||
rl.close();
|
||||
if (answer.toLowerCase() !== "y") {
|
||||
printInfo("Cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Deleting entity...", async () => {
|
||||
return backend.deleteEntities({
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
appId: opts.appId,
|
||||
runId: opts.runId,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "entity delete",
|
||||
data: { deleted: true },
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Event commands: list and status.
|
||||
*/
|
||||
|
||||
import boxen from "boxen";
|
||||
import Table from "cli-table3";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import { colors, printError, printInfo, timedStatus } from "../branding.js";
|
||||
import { formatAgentEnvelope, formatJson } from "../output.js";
|
||||
import { setCurrentCommand } from "../state.js";
|
||||
|
||||
const { brand, accent, success, error: errorColor, warning, dim } = colors;
|
||||
|
||||
function statusStyled(status: string): string {
|
||||
switch (status.toUpperCase()) {
|
||||
case "SUCCEEDED":
|
||||
return success("SUCCEEDED");
|
||||
case "PENDING":
|
||||
return accent("PENDING");
|
||||
case "FAILED":
|
||||
return errorColor("FAILED");
|
||||
case "PROCESSING":
|
||||
return warning("PROCESSING");
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdEventList(
|
||||
backend: Backend,
|
||||
opts: { output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("event list");
|
||||
const start = performance.now();
|
||||
let results: Record<string, unknown>[];
|
||||
try {
|
||||
results = await timedStatus("Fetching events...", async () => {
|
||||
return backend.listEvents();
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "event list",
|
||||
data: results,
|
||||
count: results.length,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log();
|
||||
printInfo("No events found.");
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
accent("Event ID"),
|
||||
accent("Type"),
|
||||
accent("Status"),
|
||||
accent("Latency"),
|
||||
accent("Created"),
|
||||
],
|
||||
colWidths: [12, 14, 14, 10, 22],
|
||||
wordWrap: true,
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const ev of results) {
|
||||
const evId = String(ev.id ?? "").slice(0, 8);
|
||||
const evType = String(ev.event_type ?? "—");
|
||||
const status = String(ev.status ?? "—");
|
||||
const latency = ev.latency as number | undefined;
|
||||
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
|
||||
const created = String(ev.created_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
table.push([dim(evId), evType, statusStyled(status), latencyStr, created]);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(table.toString());
|
||||
console.log(
|
||||
` ${dim(`${results.length} event${results.length !== 1 ? "s" : ""}`)}`,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
export async function cmdEventStatus(
|
||||
backend: Backend,
|
||||
eventId: string,
|
||||
opts: { output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("event status");
|
||||
const start = performance.now();
|
||||
let ev: Record<string, unknown>;
|
||||
try {
|
||||
ev = await timedStatus("Fetching event...", async () => {
|
||||
return backend.getEvent(eventId);
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "event status",
|
||||
data: ev,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const status = String(ev.status ?? "—");
|
||||
const evType = String(ev.event_type ?? "—");
|
||||
const latency = ev.latency as number | undefined;
|
||||
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
|
||||
const created = String(ev.created_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
const updated = String(ev.updated_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
const results = ev.results as Record<string, unknown>[] | undefined;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(` ${dim("Event ID:")} ${eventId}`);
|
||||
lines.push(` ${dim("Type:")} ${evType}`);
|
||||
lines.push(` ${dim("Status:")} ${statusStyled(status)}`);
|
||||
lines.push(` ${dim("Latency:")} ${latencyStr}`);
|
||||
lines.push(` ${dim("Created:")} ${created}`);
|
||||
lines.push(` ${dim("Updated:")} ${updated}`);
|
||||
|
||||
if (results && results.length > 0) {
|
||||
lines.push("");
|
||||
lines.push(` ${dim(`Results (${results.length}):`)}`);
|
||||
for (const r of results) {
|
||||
const memId = String(r.id ?? "").slice(0, 8);
|
||||
const data = r.data as Record<string, unknown> | undefined;
|
||||
const memory = data?.memory ? String(data.memory) : "";
|
||||
const evName = String(r.event ?? "");
|
||||
const user = String(r.user_id ?? "");
|
||||
let detail = `${evName} ${memory}`;
|
||||
if (user) detail += ` ${dim(`(user_id=${user})`)}`;
|
||||
lines.push(` ${success("·")} ${detail} ${dim(`(${memId})`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const content = lines.join("\n");
|
||||
console.log();
|
||||
console.log(
|
||||
boxen(content, {
|
||||
title: brand("Event Status"),
|
||||
titleAlignment: "left",
|
||||
borderColor: "magenta",
|
||||
padding: 1,
|
||||
}),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* mem0 identify — declare which agent owns the current agent-mode key.
|
||||
*
|
||||
* Used when `mem0 init --agent` ran without --agent-caller, so the backend
|
||||
* saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
|
||||
* its own row with its real identity. Idempotent.
|
||||
*/
|
||||
|
||||
import { printError, printSuccess } from "../branding.js";
|
||||
import { loadConfig, saveConfig } from "../config.js";
|
||||
|
||||
const SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
} as const;
|
||||
|
||||
export async function runIdentify(name: string): Promise<void> {
|
||||
const config = loadConfig();
|
||||
if (!config.platform.apiKey) {
|
||||
printError("No API key configured. Run `mem0 init --agent` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.platform.agentMode) {
|
||||
printError("This command only works on unclaimed agent-mode keys.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const clean = (name ?? "").trim();
|
||||
if (!clean) {
|
||||
printError("Agent name is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/caller/`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
...SOURCE_HEADERS,
|
||||
Authorization: `Token ${config.platform.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ agent_caller: clean }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
} catch (err) {
|
||||
printError(
|
||||
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
let detail: string = resp.statusText;
|
||||
try {
|
||||
const body = (await resp.json()) as { error?: string };
|
||||
if (body.error) detail = body.error;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Identify failed: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const body = (await resp.json()) as { agent_caller?: string };
|
||||
const canonical = body.agent_caller ?? clean;
|
||||
config.platform.agentCaller = canonical;
|
||||
saveConfig(config);
|
||||
printSuccess(`Identified as ${canonical}.`);
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* mem0 init — interactive setup wizard.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import readline from "node:readline";
|
||||
import { PlatformBackend } from "../backend/platform.js";
|
||||
import {
|
||||
colors,
|
||||
printBanner,
|
||||
printError,
|
||||
printInfo,
|
||||
printSuccess,
|
||||
} from "../branding.js";
|
||||
import {
|
||||
CONFIG_FILE,
|
||||
DEFAULT_BASE_URL,
|
||||
type Mem0Config,
|
||||
createDefaultConfig,
|
||||
loadConfig,
|
||||
redactKey,
|
||||
saveConfig,
|
||||
} from "../config.js";
|
||||
import { formatJsonEnvelope } from "../output.js";
|
||||
import { isAgentMode } from "../state.js";
|
||||
|
||||
const { brand, dim } = colors;
|
||||
|
||||
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
||||
|
||||
function validateEmail(email: string): void {
|
||||
if (!EMAIL_RE.test(email)) {
|
||||
printError(`Invalid email address: ${JSON.stringify(email)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests. */
|
||||
export async function pingKey(
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
timeoutMs = 5000,
|
||||
): Promise<boolean> {
|
||||
// Returns false ONLY on a definitive "invalid key" signal (HTTP 401/403).
|
||||
// Network errors, timeouts, and 5xx responses return true so we prefer
|
||||
// reusing an existing key over silently minting a new shadow on a transient
|
||||
// blip (which would also clobber config + plugin-sync targets).
|
||||
try {
|
||||
const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/ping/`, {
|
||||
headers: { Authorization: `Token ${apiKey}` },
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
return resp.status !== 401 && resp.status !== 403;
|
||||
} catch {
|
||||
return true; // unknown — prefer reuse
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeIdentify(
|
||||
key: string,
|
||||
baseUrl: string,
|
||||
agentCaller: string | undefined,
|
||||
): Promise<void> {
|
||||
// Best-effort PATCH agent_caller when --agent-caller is supplied on a
|
||||
// reused key. Silent no-op on any failure — reuse must not break.
|
||||
if (!agentCaller) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${baseUrl.replace(/\/+$/, "")}/api/v1/auth/agent_mode/caller/`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: `Token ${key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ agent_caller: agentCaller }),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
},
|
||||
);
|
||||
if (resp.ok) {
|
||||
try {
|
||||
const body = (await resp.json()) as { agent_caller?: string };
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const cfg = loadConfig();
|
||||
cfg.platform.agentCaller = body.agent_caller ?? agentCaller;
|
||||
saveConfig(cfg);
|
||||
}
|
||||
} catch {
|
||||
/* swallow — best effort */
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* swallow — best effort */
|
||||
}
|
||||
}
|
||||
|
||||
async function emailLogin(
|
||||
email: string,
|
||||
code: string | undefined,
|
||||
baseUrl: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const url = baseUrl.replace(/\/+$/, "");
|
||||
let codeValue = code;
|
||||
|
||||
const sourceHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
};
|
||||
|
||||
if (!codeValue) {
|
||||
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
|
||||
method: "POST",
|
||||
headers: sourceHeaders,
|
||||
body: JSON.stringify({ email }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (resp.status === 429) {
|
||||
printError("Too many attempts. Try again in a few minutes.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
let detail: string;
|
||||
try {
|
||||
const body = (await resp.json()) as Record<string, unknown>;
|
||||
detail = (body.error ?? body.detail ?? resp.statusText) as string;
|
||||
} catch {
|
||||
detail = resp.statusText;
|
||||
}
|
||||
printError(`Failed to send code: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
printSuccess("Verification code sent! Check your email.");
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
printError(
|
||||
"No --code provided and terminal is non-interactive.",
|
||||
"Run: mem0 init --email <email> --code <code>",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log();
|
||||
const entered = await promptLine(` ${brand("Verification Code")}`);
|
||||
if (!entered) {
|
||||
printError("Code is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
codeValue = entered;
|
||||
}
|
||||
|
||||
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
|
||||
method: "POST",
|
||||
headers: sourceHeaders,
|
||||
body: JSON.stringify({ email, code: codeValue.trim() }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (verifyResp.status === 429) {
|
||||
printError("Too many attempts. Try again in a few minutes.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!verifyResp.ok) {
|
||||
let detail: string;
|
||||
try {
|
||||
const body = (await verifyResp.json()) as Record<string, unknown>;
|
||||
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
|
||||
} catch {
|
||||
detail = verifyResp.statusText;
|
||||
}
|
||||
printError(`Verification failed: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return verifyResp.json() as Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
function promptSecret(label: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.stdout.write(label);
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(true);
|
||||
}
|
||||
process.stdin.resume();
|
||||
process.stdin.setEncoding("utf-8");
|
||||
|
||||
const chars: string[] = [];
|
||||
|
||||
const onData = (key: string) => {
|
||||
for (const ch of key) {
|
||||
if (ch === "\r" || ch === "\n") {
|
||||
cleanup();
|
||||
process.stdout.write("\n");
|
||||
resolve(chars.join(""));
|
||||
return;
|
||||
}
|
||||
if (ch === "\x03") {
|
||||
cleanup();
|
||||
reject(new Error("Interrupted"));
|
||||
return;
|
||||
}
|
||||
if (ch === "\x7f" || ch === "\x08") {
|
||||
// backspace
|
||||
if (chars.length > 0) {
|
||||
chars.pop();
|
||||
process.stdout.write("\b \b");
|
||||
}
|
||||
} else if (ch === "\x15") {
|
||||
// Ctrl+U — clear line
|
||||
process.stdout.write("\b \b".repeat(chars.length));
|
||||
chars.length = 0;
|
||||
} else if (ch >= " ") {
|
||||
chars.push(ch);
|
||||
process.stdout.write("*");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
process.stdin.removeListener("data", onData);
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(false);
|
||||
}
|
||||
process.stdin.pause();
|
||||
};
|
||||
|
||||
process.stdin.on("data", onData);
|
||||
});
|
||||
}
|
||||
|
||||
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
||||
return new Promise((resolve) => {
|
||||
rl.question(prompt, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim() || defaultValue || "");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function setupPlatform(config: Mem0Config): Promise<void> {
|
||||
console.log();
|
||||
console.log(
|
||||
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-node")}`,
|
||||
);
|
||||
console.log();
|
||||
|
||||
process.stdout.write(` ${brand("API Key")}: `);
|
||||
const apiKey = await promptSecret("");
|
||||
if (!apiKey) {
|
||||
printError("API key is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
config.platform.apiKey = apiKey;
|
||||
config.platform.createdVia = "api_key";
|
||||
}
|
||||
|
||||
async function setupDefaults(config: Mem0Config): Promise<void> {
|
||||
console.log();
|
||||
printInfo("Set default entity IDs (press Enter to skip).\n");
|
||||
|
||||
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
const userId = await promptLine(
|
||||
` ${brand("Default User ID")} ${dim("(recommended)")}`,
|
||||
_systemUser,
|
||||
);
|
||||
if (userId) config.defaults.userId = userId;
|
||||
}
|
||||
|
||||
async function validatePlatform(config: Mem0Config): Promise<void> {
|
||||
console.log();
|
||||
printInfo("Validating connection...");
|
||||
try {
|
||||
const backend = new PlatformBackend(config.platform);
|
||||
const status = await backend.status({
|
||||
userId: config.defaults.userId || undefined,
|
||||
agentId: config.defaults.agentId || undefined,
|
||||
});
|
||||
if (status.connected) {
|
||||
printSuccess("Connected to mem0 Platform!");
|
||||
// Cache user_email from ping response for telemetry distinct_id
|
||||
try {
|
||||
const pingData = (await backend.ping()) as Record<string, unknown>;
|
||||
const userEmail = pingData?.user_email as string | undefined;
|
||||
if (userEmail) {
|
||||
config.platform.userEmail = userEmail;
|
||||
}
|
||||
} catch {
|
||||
/* ignore — telemetry ID will fall back to API key hash */
|
||||
}
|
||||
} else {
|
||||
printError(
|
||||
`Could not connect: ${status.error ?? "Unknown error"}`,
|
||||
"Visit https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-node to get a new key, or run mem0 init again.",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runInit(
|
||||
opts: {
|
||||
apiKey?: string;
|
||||
userId?: string;
|
||||
email?: string;
|
||||
code?: string;
|
||||
force?: boolean;
|
||||
agent?: boolean;
|
||||
source?: string;
|
||||
agentCaller?: string;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const { detectAgentCaller } = await import("../agent-detect.js");
|
||||
const { bootstrapViaBackend, claimViaOtp } = await import("./agent-mode.js");
|
||||
const { isAgentMode } = await import("../state.js");
|
||||
const { captureEvent } = await import("../telemetry.js");
|
||||
|
||||
const fireInit = (
|
||||
mode: "agent" | "email" | "api_key" | "existing_key",
|
||||
claimed = false,
|
||||
) => {
|
||||
const props: Record<string, unknown> = { command: "init", mode };
|
||||
// Self-declared via --agent-caller; not sniffed from env vars.
|
||||
if (opts.agentCaller) props.agent_caller = opts.agentCaller;
|
||||
if (opts.source) props.signup_source = opts.source;
|
||||
if (claimed) props.claimed_agent_mode = true;
|
||||
captureEvent("cli.init", props);
|
||||
};
|
||||
|
||||
const config = createDefaultConfig();
|
||||
const savedConfig = loadConfig();
|
||||
const baseUrl =
|
||||
process.env.MEM0_BASE_URL ||
|
||||
savedConfig.platform.baseUrl ||
|
||||
DEFAULT_BASE_URL;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
|
||||
// Guards
|
||||
if (opts.code && !opts.email) {
|
||||
printError("--code requires --email.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (opts.email && opts.apiKey) {
|
||||
printError("Cannot use both --api-key and --email.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Claim flow: --email against an existing agent-mode config ───────────
|
||||
if (
|
||||
opts.email &&
|
||||
fs.existsSync(CONFIG_FILE) &&
|
||||
savedConfig.platform.agentMode &&
|
||||
savedConfig.platform.apiKey
|
||||
) {
|
||||
const email = opts.email.trim().toLowerCase();
|
||||
validateEmail(email);
|
||||
printInfo(`Claiming Agent Mode account to ${email}...`);
|
||||
await claimViaOtp(savedConfig, { email, code: opts.code });
|
||||
fireInit("email", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Agent Mode path runs BEFORE the existing-config guard ──────────────
|
||||
// Rule 1/2 will REUSE a valid existing key (not overwrite), so we must
|
||||
// short-circuit before the guard prompts the user about overwriting.
|
||||
// Rule 3 only mints when there's no valid key to reuse — in that case
|
||||
// overwriting is what the user wants.
|
||||
const agentCtx =
|
||||
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
|
||||
if (!opts.apiKey && !opts.email && agentCtx) {
|
||||
const emitReuseEnvelope = (source: "env" | "config") => {
|
||||
if (isAgentMode()) {
|
||||
formatJsonEnvelope({
|
||||
command: "init",
|
||||
data: {
|
||||
api_key_saved: false,
|
||||
api_key_source: source,
|
||||
agent_mode: false,
|
||||
message:
|
||||
"Existing Mem0 API key found and reused. No Agent Mode key was created.",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
printSuccess(
|
||||
source === "env"
|
||||
? "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
|
||||
: "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.",
|
||||
);
|
||||
}
|
||||
};
|
||||
// Rule 1: env MEM0_API_KEY valid → reuse, no new key.
|
||||
const envKey = (process.env.MEM0_API_KEY || "").trim();
|
||||
if (envKey && (await pingKey(envKey, baseUrl))) {
|
||||
await maybeIdentify(envKey, baseUrl, opts.agentCaller);
|
||||
emitReuseEnvelope("env");
|
||||
fireInit("existing_key");
|
||||
return;
|
||||
}
|
||||
// Rule 2: existing config api_key valid → reuse.
|
||||
if (
|
||||
savedConfig.platform.apiKey &&
|
||||
(await pingKey(savedConfig.platform.apiKey, baseUrl))
|
||||
) {
|
||||
await maybeIdentify(
|
||||
savedConfig.platform.apiKey,
|
||||
baseUrl,
|
||||
opts.agentCaller,
|
||||
);
|
||||
emitReuseEnvelope("config");
|
||||
fireInit("existing_key");
|
||||
return;
|
||||
}
|
||||
// Rule 3: mint a fresh shadow (no valid key to reuse).
|
||||
// agent_caller is self-declared via --agent-caller (Proof Editor-style),
|
||||
// not derived from env-var sniffing. detectAgentCaller() above is still
|
||||
// used as a context trigger (does this look like an agent?) but never
|
||||
// to fill identity.
|
||||
await bootstrapViaBackend(config, {
|
||||
source: opts.source ?? null,
|
||||
agentCaller: opts.agentCaller ?? null,
|
||||
});
|
||||
fireInit("agent");
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn if an existing config with an API key would be overwritten
|
||||
if (
|
||||
!opts.force &&
|
||||
fs.existsSync(CONFIG_FILE) &&
|
||||
savedConfig.platform.apiKey
|
||||
) {
|
||||
console.log(
|
||||
`\n ${brand("Existing configuration found")} ${dim(`(API key: ${redactKey(savedConfig.platform.apiKey)})`)}`,
|
||||
);
|
||||
if (process.stdin.isTTY) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
" Overwrite existing config? This cannot be undone. [y/N] ",
|
||||
resolve,
|
||||
);
|
||||
});
|
||||
rl.close();
|
||||
if (answer.toLowerCase() !== "y") {
|
||||
printInfo("Cancelled. Use --force to skip this check.");
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
printError(
|
||||
"Existing config would be overwritten.",
|
||||
"Use --force to overwrite.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Email login flow ──────────────────────────────────────────────────────
|
||||
if (opts.email) {
|
||||
const email = opts.email.trim().toLowerCase();
|
||||
validateEmail(email);
|
||||
|
||||
printBanner();
|
||||
console.log();
|
||||
printInfo(`Logging in as ${email}...\n`);
|
||||
|
||||
const result = await emailLogin(email, opts.code, baseUrl);
|
||||
|
||||
const apiKeyVal = result.api_key as string | undefined;
|
||||
if (!apiKeyVal) {
|
||||
printError(
|
||||
"Auth succeeded but no API key was returned. Contact support.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.platform.apiKey = apiKeyVal;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
config.defaults.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
|
||||
saveConfig(config);
|
||||
console.log();
|
||||
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
|
||||
console.log();
|
||||
console.log(` ${dim("Get started:")}`);
|
||||
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
|
||||
console.log(` ${dim(' mem0 search "preferences"')}`);
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── API key flow ──────────────────────────────────────────────────────────
|
||||
// (Agent Mode branch runs earlier — see above, before the existing-config
|
||||
// guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
|
||||
|
||||
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||
if (!process.stdin.isTTY) {
|
||||
if (!opts.apiKey) {
|
||||
printError(
|
||||
"Non-interactive terminal detected and --api-key is required.",
|
||||
"Usage: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
opts.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
}
|
||||
|
||||
// Non-interactive: both flags provided
|
||||
if (opts.apiKey && opts.userId) {
|
||||
config.platform.apiKey = opts.apiKey;
|
||||
config.platform.createdVia = "api_key";
|
||||
config.defaults.userId = opts.userId;
|
||||
await validatePlatform(config);
|
||||
saveConfig(config);
|
||||
printSuccess("Configuration saved to ~/.mem0/config.json");
|
||||
return;
|
||||
}
|
||||
|
||||
printBanner();
|
||||
console.log();
|
||||
printInfo("Welcome! Let's set up your mem0 CLI.\n");
|
||||
|
||||
// Use provided API key or prompt
|
||||
if (opts.apiKey) {
|
||||
config.platform.apiKey = opts.apiKey;
|
||||
} else {
|
||||
console.log(` ${brand("How would you like to authenticate?")}`);
|
||||
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
|
||||
console.log(` ${dim("2.")} Enter API key manually`);
|
||||
console.log();
|
||||
|
||||
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
|
||||
|
||||
if (choice === "1") {
|
||||
console.log();
|
||||
const emailAddr = await promptLine(` ${brand("Email")}`);
|
||||
if (!emailAddr) {
|
||||
printError("Email is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const email = emailAddr.trim().toLowerCase();
|
||||
validateEmail(email);
|
||||
printInfo(`Logging in as ${email}...\n`);
|
||||
|
||||
const result = await emailLogin(email, undefined, baseUrl);
|
||||
|
||||
const apiKeyVal = result.api_key as string | undefined;
|
||||
if (!apiKeyVal) {
|
||||
printError(
|
||||
"Auth succeeded but no API key was returned. Contact support.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.platform.apiKey = apiKeyVal;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
config.defaults.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
|
||||
saveConfig(config);
|
||||
console.log();
|
||||
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
|
||||
console.log();
|
||||
console.log(` ${dim("Get started:")}`);
|
||||
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
|
||||
console.log(` ${dim(' mem0 search "preferences"')}`);
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
// choice === "2": fall through to API key prompt
|
||||
await setupPlatform(config);
|
||||
}
|
||||
|
||||
// Use provided user ID or prompt
|
||||
if (opts.userId) {
|
||||
config.defaults.userId = opts.userId;
|
||||
} else {
|
||||
await setupDefaults(config);
|
||||
}
|
||||
|
||||
await validatePlatform(config);
|
||||
|
||||
saveConfig(config);
|
||||
console.log();
|
||||
printSuccess("Configuration saved to ~/.mem0/config.json");
|
||||
console.log();
|
||||
console.log(` ${dim("Get started:")}`);
|
||||
if (config.defaults.userId) {
|
||||
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
|
||||
console.log(` ${dim(' mem0 search "preferences"')}`);
|
||||
} else {
|
||||
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
|
||||
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
/**
|
||||
* Memory CRUD commands: add, search, get, list, update, delete.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import {
|
||||
printError,
|
||||
printInfo,
|
||||
printScope,
|
||||
printSuccess,
|
||||
timedStatus,
|
||||
} from "../branding.js";
|
||||
import {
|
||||
formatAddResult,
|
||||
formatAgentEnvelope,
|
||||
formatJson,
|
||||
formatJsonEnvelope,
|
||||
formatMemoriesTable,
|
||||
formatMemoriesText,
|
||||
formatSingleMemory,
|
||||
printResultSummary,
|
||||
} from "../output.js";
|
||||
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||
|
||||
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
|
||||
function _stdinIsPiped(): boolean {
|
||||
if (isAgentMode()) return false;
|
||||
try {
|
||||
const stat = fs.fstatSync(0);
|
||||
return stat.isFIFO() || stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdAdd(
|
||||
backend: Backend,
|
||||
text: string | undefined,
|
||||
opts: {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
messages?: string;
|
||||
file?: string;
|
||||
metadata?: string;
|
||||
immutable: boolean;
|
||||
noInfer: boolean;
|
||||
expires?: string;
|
||||
categories?: string;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("add");
|
||||
let msgs: Record<string, unknown>[] | undefined;
|
||||
let content = text;
|
||||
|
||||
// Read from file
|
||||
if (opts.file) {
|
||||
try {
|
||||
const raw = fs.readFileSync(opts.file, "utf-8");
|
||||
msgs = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
// Parse messages JSON
|
||||
else if (opts.messages) {
|
||||
try {
|
||||
msgs = JSON.parse(opts.messages);
|
||||
} catch (e) {
|
||||
printError(
|
||||
`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
// Read from stdin only if stdin is an actual pipe or file redirect
|
||||
else if (!content && _stdinIsPiped()) {
|
||||
content = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
|
||||
if (content !== undefined && content.trim() === "") {
|
||||
printError("Content cannot be empty.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!content && !msgs) {
|
||||
printError(
|
||||
"No content provided. Pass text, --messages, --file, or pipe via stdin.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate --expires
|
||||
if (opts.expires) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
|
||||
printError(
|
||||
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (new Date(opts.expires) <= new Date()) {
|
||||
printError("--expires date must be in the future.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let meta: Record<string, unknown> | undefined;
|
||||
if (opts.metadata) {
|
||||
try {
|
||||
meta = JSON.parse(opts.metadata);
|
||||
} catch {
|
||||
printError("Invalid JSON in --metadata.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let cats: string[] | undefined;
|
||||
if (opts.categories) {
|
||||
try {
|
||||
cats = JSON.parse(opts.categories);
|
||||
} catch {
|
||||
cats = opts.categories.split(",").map((c) => c.trim());
|
||||
}
|
||||
}
|
||||
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Adding memory...", async () => {
|
||||
return backend.add(content ?? undefined, msgs, {
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
appId: opts.appId,
|
||||
runId: opts.runId,
|
||||
metadata: meta,
|
||||
immutable: opts.immutable,
|
||||
infer: !opts.noInfer,
|
||||
expires: opts.expires,
|
||||
categories: cats,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (opts.output === "quiet") return;
|
||||
|
||||
// Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||
const rawResults: Record<string, unknown>[] = Array.isArray(result)
|
||||
? result
|
||||
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
||||
const seenEvents = new Set<string>();
|
||||
const deduped: Record<string, unknown>[] = [];
|
||||
for (const r of rawResults) {
|
||||
if (r.status === "PENDING") {
|
||||
const eid = (r.event_id as string) ?? "";
|
||||
if (eid && seenEvents.has(eid)) continue;
|
||||
if (eid) seenEvents.add(eid);
|
||||
}
|
||||
deduped.push(r);
|
||||
}
|
||||
// Write back so downstream formatters see deduplicated data
|
||||
const dedupedResult: Record<string, unknown> = Array.isArray(result)
|
||||
? (deduped as unknown as Record<string, unknown>)
|
||||
: { ...result, results: deduped };
|
||||
|
||||
if (opts.output === "agent") {
|
||||
const scope: Record<string, string | undefined> = {
|
||||
user_id: opts.userId,
|
||||
agent_id: opts.agentId,
|
||||
app_id: opts.appId,
|
||||
run_id: opts.runId,
|
||||
};
|
||||
formatAgentEnvelope({
|
||||
command: "add",
|
||||
data: deduped,
|
||||
scope,
|
||||
count: deduped.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.output === "json") {
|
||||
formatAddResult(dedupedResult, opts.output);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
printScope({
|
||||
user_id: opts.userId,
|
||||
agent_id: opts.agentId,
|
||||
app_id: opts.appId,
|
||||
run_id: opts.runId,
|
||||
});
|
||||
const count = deduped.length;
|
||||
const allPending = count > 0 && deduped.every((r) => r.status === "PENDING");
|
||||
if (allPending) {
|
||||
printSuccess(
|
||||
`Memory queued — ${count} event${count !== 1 ? "s" : ""} pending`,
|
||||
);
|
||||
} else {
|
||||
printSuccess(
|
||||
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
|
||||
);
|
||||
}
|
||||
formatAddResult(dedupedResult, opts.output);
|
||||
}
|
||||
|
||||
export async function cmdSearch(
|
||||
backend: Backend,
|
||||
query: string | undefined,
|
||||
opts: {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
topK: number;
|
||||
threshold: number;
|
||||
rerank: boolean;
|
||||
keyword: boolean;
|
||||
filterJson?: string;
|
||||
fields?: string;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("search");
|
||||
if (!query) {
|
||||
printError("No query provided. Pass a query argument or pipe via stdin.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let filters: Record<string, unknown> | undefined;
|
||||
if (opts.filterJson) {
|
||||
try {
|
||||
filters = JSON.parse(opts.filterJson);
|
||||
} catch {
|
||||
printError("Invalid JSON in --filter.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const fieldList = opts.fields
|
||||
? opts.fields.split(",").map((f) => f.trim())
|
||||
: undefined;
|
||||
|
||||
if (opts.topK < 1) {
|
||||
printError("--top-k must be >= 1.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (opts.threshold < 0 || opts.threshold > 1) {
|
||||
printError("--threshold must be between 0.0 and 1.0.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let results: Record<string, unknown>[];
|
||||
try {
|
||||
results = await timedStatus("Searching memories...", async () => {
|
||||
// biome-ignore lint/style/noNonNullAssertion: guarded by process.exit above
|
||||
return backend.search(query!, {
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
appId: opts.appId,
|
||||
runId: opts.runId,
|
||||
topK: opts.topK,
|
||||
threshold: opts.threshold,
|
||||
rerank: opts.rerank,
|
||||
keyword: opts.keyword,
|
||||
filters,
|
||||
fields: fieldList,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "quiet") return;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
const scope: Record<string, string | undefined> = {
|
||||
user_id: opts.userId,
|
||||
agent_id: opts.agentId,
|
||||
app_id: opts.appId,
|
||||
run_id: opts.runId,
|
||||
};
|
||||
formatAgentEnvelope({
|
||||
command: "search",
|
||||
data: results,
|
||||
scope,
|
||||
count: results.length,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.output === "json") {
|
||||
formatJson(results);
|
||||
} else if (opts.output === "table") {
|
||||
if (results.length > 0) {
|
||||
formatMemoriesTable(results, { showScore: true });
|
||||
printResultSummary({
|
||||
count: results.length,
|
||||
durationSecs: elapsed,
|
||||
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||
});
|
||||
} else {
|
||||
console.log();
|
||||
printInfo("No memories found matching your query.");
|
||||
console.log();
|
||||
}
|
||||
} else {
|
||||
if (results.length > 0) {
|
||||
formatMemoriesText(results);
|
||||
printResultSummary({
|
||||
count: results.length,
|
||||
durationSecs: elapsed,
|
||||
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||
});
|
||||
} else {
|
||||
console.log();
|
||||
printInfo("No memories found matching your query.");
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdGet(
|
||||
backend: Backend,
|
||||
memoryId: string,
|
||||
opts: { output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("get");
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Fetching memory...", async () => {
|
||||
return backend.get(memoryId);
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({ command: "get", data: result });
|
||||
} else {
|
||||
formatSingleMemory(result, opts.output);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdList(
|
||||
backend: Backend,
|
||||
opts: {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
category?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("list");
|
||||
if (opts.pageSize < 1) {
|
||||
printError("--page-size must be >= 1.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (opts.page < 1) {
|
||||
printError("--page must be >= 1.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let results: Record<string, unknown>[];
|
||||
try {
|
||||
results = await timedStatus("Listing memories...", async () => {
|
||||
return backend.listMemories({
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
appId: opts.appId,
|
||||
runId: opts.runId,
|
||||
page: opts.page,
|
||||
pageSize: opts.pageSize,
|
||||
category: opts.category,
|
||||
after: opts.after,
|
||||
before: opts.before,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "quiet") return;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
const scope: Record<string, string | undefined> = {
|
||||
user_id: opts.userId,
|
||||
agent_id: opts.agentId,
|
||||
app_id: opts.appId,
|
||||
run_id: opts.runId,
|
||||
};
|
||||
formatAgentEnvelope({
|
||||
command: "list",
|
||||
data: results,
|
||||
scope,
|
||||
count: results.length,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "table") {
|
||||
if (results.length > 0) {
|
||||
formatMemoriesTable(results);
|
||||
printResultSummary({
|
||||
count: results.length,
|
||||
durationSecs: elapsed,
|
||||
page: opts.page,
|
||||
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||
});
|
||||
} else {
|
||||
console.log();
|
||||
printInfo("No memories found.");
|
||||
console.log();
|
||||
}
|
||||
} else {
|
||||
if (results.length > 0) {
|
||||
formatMemoriesText(results, "memories");
|
||||
printResultSummary({
|
||||
count: results.length,
|
||||
durationSecs: elapsed,
|
||||
page: opts.page,
|
||||
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||
});
|
||||
} else {
|
||||
console.log();
|
||||
printInfo("No memories found.");
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdUpdate(
|
||||
backend: Backend,
|
||||
memoryId: string,
|
||||
text: string | undefined,
|
||||
opts: { metadata?: string; output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("update");
|
||||
let meta: Record<string, unknown> | undefined;
|
||||
if (opts.metadata) {
|
||||
try {
|
||||
meta = JSON.parse(opts.metadata);
|
||||
} catch {
|
||||
printError("Invalid JSON in --metadata.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Updating memory...", async () => {
|
||||
return backend.update(memoryId, text, meta);
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "update",
|
||||
data: result,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
printSuccess(
|
||||
`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdDelete(
|
||||
backend: Backend,
|
||||
memoryId: string,
|
||||
opts: { output: string; dryRun?: boolean; force?: boolean },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("delete");
|
||||
if (opts.dryRun) {
|
||||
let mem: Record<string, unknown>;
|
||||
try {
|
||||
mem = await backend.get(memoryId);
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const text = (mem.memory ?? mem.text ?? "") as string;
|
||||
printInfo(`Would delete memory ${memoryId.slice(0, 8)}: ${text}`);
|
||||
printInfo("No changes made.");
|
||||
return;
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Deleting...", async () => {
|
||||
return backend.delete(memoryId);
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "delete",
|
||||
data: { id: memoryId, deleted: true },
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
printSuccess(
|
||||
`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdDeleteAll(
|
||||
backend: Backend,
|
||||
opts: {
|
||||
force: boolean;
|
||||
dryRun?: boolean;
|
||||
all?: boolean;
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("delete-all");
|
||||
const { isAgentMode } = await import("../state.js");
|
||||
if (isAgentMode() && !opts.force) {
|
||||
printError("Destructive operation requires --force in agent mode.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (opts.all) {
|
||||
// Project-wide wipe using wildcard entity IDs
|
||||
// Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||
|
||||
if (!opts.force) {
|
||||
const readline = await import("node:readline");
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
"\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] ",
|
||||
resolve,
|
||||
);
|
||||
});
|
||||
rl.close();
|
||||
if (answer.toLowerCase() !== "y") {
|
||||
printInfo("Cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus(
|
||||
"Deleting all memories project-wide...",
|
||||
async () => {
|
||||
return backend.delete(undefined, {
|
||||
all: true,
|
||||
userId: "*",
|
||||
agentId: "*",
|
||||
appId: "*",
|
||||
runId: "*",
|
||||
});
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "delete-all",
|
||||
data: result,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
if (result.message) {
|
||||
printInfo(
|
||||
"Deletion started. Memories will be removed in the background.",
|
||||
);
|
||||
} else {
|
||||
printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.dryRun) {
|
||||
let memories: Record<string, unknown>[];
|
||||
try {
|
||||
memories = await backend.listMemories({
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
appId: opts.appId,
|
||||
runId: opts.runId,
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
printInfo(`Would delete ${memories.length} memories.`);
|
||||
printInfo("No changes made.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.force) {
|
||||
const scopeParts: string[] = [];
|
||||
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||
const scope =
|
||||
scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
|
||||
|
||||
const readline = await import("node:readline");
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `,
|
||||
resolve,
|
||||
);
|
||||
});
|
||||
rl.close();
|
||||
if (answer.toLowerCase() !== "y") {
|
||||
printInfo("Cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Deleting all memories...", async () => {
|
||||
return backend.delete(undefined, {
|
||||
all: true,
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
appId: opts.appId,
|
||||
runId: opts.runId,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "delete-all",
|
||||
data: result,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
if (result.message) {
|
||||
printInfo(
|
||||
"Deletion started. Memories will be removed in the background.",
|
||||
);
|
||||
} else {
|
||||
printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Utility commands: status, version, import.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import boxen from "boxen";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
|
||||
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||
import { setCurrentCommand } from "../state.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
|
||||
const { brand, dim, success, error: errorColor } = colors;
|
||||
|
||||
export async function cmdStatus(
|
||||
backend: Backend,
|
||||
opts: { userId?: string; agentId?: string; output?: string } = {},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("status");
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Checking connection...", async () => {
|
||||
return backend.status({ userId: opts.userId, agentId: opts.agentId });
|
||||
});
|
||||
} catch (e) {
|
||||
result = {
|
||||
connected: false,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "status",
|
||||
data: {
|
||||
connected: result.connected,
|
||||
backend: result.backend ?? null,
|
||||
base_url: result.base_url ?? null,
|
||||
},
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
if (result.connected) {
|
||||
lines.push(` ${success("\u25cf")} Connected`);
|
||||
} else {
|
||||
lines.push(` ${errorColor("\u25cf")} Disconnected`);
|
||||
}
|
||||
|
||||
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
|
||||
if (result.base_url) {
|
||||
lines.push(` ${dim("API URL:")} ${result.base_url}`);
|
||||
}
|
||||
if (result.error) {
|
||||
lines.push(` ${errorColor("Error:")} ${result.error}`);
|
||||
if (String(result.error).includes("Authentication failed")) {
|
||||
lines.push("");
|
||||
lines.push(
|
||||
` ${dim("Run")} ${brand("mem0 init")} ${dim("to reconfigure your API key")}`,
|
||||
);
|
||||
lines.push(
|
||||
` ${dim("Get a key at")} ${brand("https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-node")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
|
||||
|
||||
const content = lines.join("\n");
|
||||
console.log();
|
||||
console.log(
|
||||
boxen(content, {
|
||||
title: brand("Connection Status"),
|
||||
titleAlignment: "left",
|
||||
borderColor: "magenta",
|
||||
padding: 1,
|
||||
}),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
export function cmdVersion(): void {
|
||||
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
}
|
||||
|
||||
export async function cmdImport(
|
||||
backend: Backend,
|
||||
filePath: string,
|
||||
opts: { userId?: string; agentId?: string; output?: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("import");
|
||||
let data: Record<string, unknown>[];
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
data = Array.isArray(parsed) ? parsed : [parsed];
|
||||
} catch (e) {
|
||||
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let added = 0;
|
||||
let failed = 0;
|
||||
const start = performance.now();
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i];
|
||||
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
|
||||
if (!content) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await backend.add(content, undefined, {
|
||||
userId: opts.userId ?? (item.user_id as string | undefined),
|
||||
agentId: opts.agentId ?? (item.agent_id as string | undefined),
|
||||
metadata: item.metadata as Record<string, unknown> | undefined,
|
||||
});
|
||||
added++;
|
||||
} catch {
|
||||
failed++;
|
||||
}
|
||||
|
||||
// Simple progress indicator
|
||||
if ((i + 1) % 10 === 0 || i === data.length - 1) {
|
||||
process.stdout.write(
|
||||
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
console.log(); // Clear progress line
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "import",
|
||||
data: {
|
||||
added,
|
||||
failed,
|
||||
},
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
|
||||
if (failed > 0) {
|
||||
printError(`${failed} memories failed to import.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Configuration management for mem0 CLI.
|
||||
*
|
||||
* Config precedence (highest to lowest):
|
||||
* 1. CLI flags (--api-key, --base-url, etc.)
|
||||
* 2. Environment variables (MEM0_API_KEY, etc.)
|
||||
* 3. Config file (~/.mem0/config.json)
|
||||
* 4. Defaults
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
export const CONFIG_DIR = path.join(os.homedir(), ".mem0");
|
||||
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
||||
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
|
||||
export const CONFIG_VERSION = 1;
|
||||
|
||||
export interface PlatformConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
userEmail: string;
|
||||
// Agent Mode (unclaimed-shadow signup)
|
||||
agentMode: boolean; // true while the key is an unclaimed agent-mode key
|
||||
createdVia: string; // "agent_mode" | "email" | "api_key" | "existing_key"
|
||||
agentCaller: string; // canonical agent name when createdVia === "agent_mode" (e.g. "claude-code")
|
||||
claimedAt: string; // ISO timestamp once the agent has been claimed
|
||||
defaultUserId: string; // `user_<slug>` returned by bootstrap; auto-default scope
|
||||
}
|
||||
|
||||
export interface DefaultsConfig {
|
||||
userId: string;
|
||||
agentId: string;
|
||||
appId: string;
|
||||
runId: string;
|
||||
}
|
||||
|
||||
export interface TelemetryConfig {
|
||||
anonymousId: string;
|
||||
}
|
||||
|
||||
export interface Mem0Config {
|
||||
version: number;
|
||||
defaults: DefaultsConfig;
|
||||
platform: PlatformConfig;
|
||||
telemetry: TelemetryConfig;
|
||||
}
|
||||
|
||||
export function createDefaultConfig(): Mem0Config {
|
||||
return {
|
||||
version: CONFIG_VERSION,
|
||||
defaults: {
|
||||
userId: "",
|
||||
agentId: "",
|
||||
appId: "",
|
||||
runId: "",
|
||||
},
|
||||
platform: {
|
||||
apiKey: "",
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
userEmail: "",
|
||||
agentMode: false,
|
||||
createdVia: "",
|
||||
agentCaller: "",
|
||||
claimedAt: "",
|
||||
defaultUserId: "",
|
||||
},
|
||||
telemetry: {
|
||||
anonymousId: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function ensureConfigDir(): string {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
||||
return CONFIG_DIR;
|
||||
}
|
||||
|
||||
export function loadConfig(): Mem0Config {
|
||||
const config = createDefaultConfig();
|
||||
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
const data = JSON.parse(raw);
|
||||
|
||||
config.version = data.version ?? CONFIG_VERSION;
|
||||
|
||||
const plat = data.platform ?? {};
|
||||
config.platform.apiKey = plat.api_key ?? "";
|
||||
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
||||
config.platform.userEmail = plat.user_email ?? "";
|
||||
config.platform.agentMode = Boolean(plat.agent_mode ?? false);
|
||||
config.platform.createdVia = plat.created_via ?? "";
|
||||
config.platform.agentCaller = plat.agent_caller ?? "";
|
||||
config.platform.claimedAt = plat.claimed_at ?? "";
|
||||
config.platform.defaultUserId = plat.default_user_id ?? "";
|
||||
|
||||
const defaults = data.defaults ?? {};
|
||||
config.defaults.userId = defaults.user_id ?? "";
|
||||
config.defaults.agentId = defaults.agent_id ?? "";
|
||||
config.defaults.appId = defaults.app_id ?? "";
|
||||
config.defaults.runId = defaults.run_id ?? "";
|
||||
const telemetry = data.telemetry ?? {};
|
||||
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
|
||||
}
|
||||
|
||||
// Environment variable overrides
|
||||
if (process.env.MEM0_API_KEY)
|
||||
config.platform.apiKey = process.env.MEM0_API_KEY;
|
||||
if (process.env.MEM0_BASE_URL)
|
||||
config.platform.baseUrl = process.env.MEM0_BASE_URL;
|
||||
if (process.env.MEM0_USER_ID)
|
||||
config.defaults.userId = process.env.MEM0_USER_ID;
|
||||
if (process.env.MEM0_AGENT_ID)
|
||||
config.defaults.agentId = process.env.MEM0_AGENT_ID;
|
||||
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
|
||||
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
|
||||
return config;
|
||||
}
|
||||
|
||||
export function saveConfig(config: Mem0Config): void {
|
||||
ensureConfigDir();
|
||||
|
||||
const data = {
|
||||
version: config.version,
|
||||
defaults: {
|
||||
user_id: config.defaults.userId,
|
||||
agent_id: config.defaults.agentId,
|
||||
app_id: config.defaults.appId,
|
||||
run_id: config.defaults.runId,
|
||||
},
|
||||
platform: {
|
||||
api_key: config.platform.apiKey,
|
||||
base_url: config.platform.baseUrl,
|
||||
user_email: config.platform.userEmail,
|
||||
agent_mode: config.platform.agentMode,
|
||||
created_via: config.platform.createdVia,
|
||||
agent_caller: config.platform.agentCaller,
|
||||
claimed_at: config.platform.claimedAt,
|
||||
default_user_id: config.platform.defaultUserId,
|
||||
},
|
||||
telemetry: {
|
||||
anonymous_id: config.telemetry.anonymousId,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
||||
fs.chmodSync(CONFIG_FILE, 0o600);
|
||||
|
||||
// Propagate api_key to ecosystem touchpoints (Claude plugin env injection,
|
||||
// shell rc exports). Idempotent — updates only EXISTING entries; never
|
||||
// creates new ones. Best-effort: errors swallowed so config.json is
|
||||
// always authoritative, never blocked by plugin-state issues.
|
||||
if (config.platform.apiKey) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { syncApiKey } = require("./plugin-sync.js");
|
||||
syncApiKey(config.platform.apiKey);
|
||||
} catch {
|
||||
/* swallow */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function redactKey(key: string): string {
|
||||
if (!key) return "(not set)";
|
||||
if (key.length <= 8) return `${key.slice(0, 2)}***`;
|
||||
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
||||
}
|
||||
|
||||
/** Key map from dotted config path to the config object fields. */
|
||||
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
||||
"platform.api_key": ["platform", "apiKey"],
|
||||
"platform.base_url": ["platform", "baseUrl"],
|
||||
"platform.user_email": ["platform", "userEmail"],
|
||||
"defaults.user_id": ["defaults", "userId"],
|
||||
"defaults.agent_id": ["defaults", "agentId"],
|
||||
"defaults.app_id": ["defaults", "appId"],
|
||||
"defaults.run_id": ["defaults", "runId"],
|
||||
// Short-form aliases
|
||||
api_key: ["platform", "apiKey"],
|
||||
base_url: ["platform", "baseUrl"],
|
||||
user_email: ["platform", "userEmail"],
|
||||
user_id: ["defaults", "userId"],
|
||||
agent_id: ["defaults", "agentId"],
|
||||
app_id: ["defaults", "appId"],
|
||||
run_id: ["defaults", "runId"],
|
||||
};
|
||||
|
||||
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
|
||||
const mapping = KEY_MAP[dottedKey];
|
||||
if (!mapping) return undefined;
|
||||
const [section, field] = mapping;
|
||||
return (config[section] as unknown as Record<string, unknown>)[field];
|
||||
}
|
||||
|
||||
export function setNestedValue(
|
||||
config: Mem0Config,
|
||||
dottedKey: string,
|
||||
value: string,
|
||||
): boolean {
|
||||
const mapping = KEY_MAP[dottedKey];
|
||||
if (!mapping) return false;
|
||||
const [section, field] = mapping;
|
||||
const obj = config[section] as unknown as Record<string, unknown>;
|
||||
|
||||
const current = obj[field];
|
||||
if (typeof current === "boolean") {
|
||||
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
||||
} else if (typeof current === "number") {
|
||||
obj[field] = Number.parseInt(value, 10);
|
||||
} else {
|
||||
obj[field] = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
|
||||
declare const __CLI_VERSION__: string | undefined;
|
||||
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* Rich-style help formatter for Commander.js that matches the Python CLI's
|
||||
* Typer + Rich output (rounded box panels, brand purple, grouped options).
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import type { Argument, Command, Help, Option } from "commander";
|
||||
// Colors imported from chalk directly to match Typer/Rich defaults
|
||||
|
||||
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
||||
|
||||
const cyanBold = chalk.cyan.bold; // option flags, command names
|
||||
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
|
||||
const yellowBold = chalk.yellow.bold; // metavar <value>
|
||||
const yellow = chalk.yellow; // "Usage:" label
|
||||
const bold = chalk.bold; // command name in usage
|
||||
const dim = chalk.dim; // defaults, descriptions
|
||||
const dimBorder = chalk.dim; // panel borders
|
||||
|
||||
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
||||
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
function stripAnsi(str: string): number {
|
||||
return str.replace(ANSI_RE, "").length;
|
||||
}
|
||||
|
||||
// ── Command display order (matches Python CLI) ──────────────────────────
|
||||
|
||||
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
|
||||
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
|
||||
{
|
||||
panel: "Memory",
|
||||
commands: ["add", "search", "get", "list", "update", "delete"],
|
||||
},
|
||||
{
|
||||
panel: "Management",
|
||||
commands: ["init", "status", "import", "help", "entity", "event", "config"],
|
||||
},
|
||||
];
|
||||
|
||||
/** Flat order derived from COMMAND_GROUPS. */
|
||||
const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
|
||||
|
||||
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
|
||||
|
||||
const OPTION_PANELS: Record<string, Record<string, string>> = {
|
||||
add: {
|
||||
"--user-id": "Scope",
|
||||
"--agent-id": "Scope",
|
||||
"--app-id": "Scope",
|
||||
"--run-id": "Scope",
|
||||
"--output": "Output",
|
||||
"--api-key": "Connection",
|
||||
"--base-url": "Connection",
|
||||
},
|
||||
search: {
|
||||
"--user-id": "Scope",
|
||||
"--agent-id": "Scope",
|
||||
"--app-id": "Scope",
|
||||
"--run-id": "Scope",
|
||||
"--top-k": "Search",
|
||||
"--threshold": "Search",
|
||||
"--rerank": "Search",
|
||||
"--keyword": "Search",
|
||||
"--filter": "Search",
|
||||
"--fields": "Search",
|
||||
"--graph": "Search",
|
||||
"--no-graph": "Search",
|
||||
"--output": "Output",
|
||||
"--api-key": "Connection",
|
||||
"--base-url": "Connection",
|
||||
},
|
||||
get: {
|
||||
"--output": "Output",
|
||||
"--api-key": "Connection",
|
||||
"--base-url": "Connection",
|
||||
},
|
||||
list: {
|
||||
"--user-id": "Scope",
|
||||
"--agent-id": "Scope",
|
||||
"--app-id": "Scope",
|
||||
"--run-id": "Scope",
|
||||
"--page": "Pagination",
|
||||
"--page-size": "Pagination",
|
||||
"--category": "Filters",
|
||||
"--after": "Filters",
|
||||
"--before": "Filters",
|
||||
"--graph": "Filters",
|
||||
"--no-graph": "Filters",
|
||||
"--output": "Output",
|
||||
"--api-key": "Connection",
|
||||
"--base-url": "Connection",
|
||||
},
|
||||
update: {
|
||||
"--output": "Output",
|
||||
"--api-key": "Connection",
|
||||
"--base-url": "Connection",
|
||||
},
|
||||
delete: {
|
||||
"--user-id": "Scope",
|
||||
"--agent-id": "Scope",
|
||||
"--app-id": "Scope",
|
||||
"--run-id": "Scope",
|
||||
"--output": "Output",
|
||||
"--api-key": "Connection",
|
||||
"--base-url": "Connection",
|
||||
},
|
||||
status: {
|
||||
"--output": "Output",
|
||||
"--api-key": "Connection",
|
||||
"--base-url": "Connection",
|
||||
},
|
||||
import: {
|
||||
"--user-id": "Scope",
|
||||
"--agent-id": "Scope",
|
||||
"--output": "Output",
|
||||
"--api-key": "Connection",
|
||||
"--base-url": "Connection",
|
||||
},
|
||||
};
|
||||
|
||||
const PANEL_ORDER: string[] = [
|
||||
"Scope",
|
||||
"Search",
|
||||
"Pagination",
|
||||
"Filters",
|
||||
"Output",
|
||||
"Connection",
|
||||
];
|
||||
|
||||
// ── Panel rendering ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Render a Rich-style ROUNDED box panel.
|
||||
*
|
||||
* ```
|
||||
* ╭─ Title ────────────────────────╮
|
||||
* │ row content padded │
|
||||
* ╰────────────────────────────────╯
|
||||
* ```
|
||||
*/
|
||||
function renderPanel(title: string, rows: string[], width: number): string {
|
||||
if (rows.length === 0) return "";
|
||||
|
||||
// Inner width is total width minus the two border chars
|
||||
const inner = width - 2;
|
||||
|
||||
// Top border: ╭─ Title ─...─╮
|
||||
const titleStr = ` ${title} `;
|
||||
const fillLen = Math.max(0, inner - 1 - titleStr.length);
|
||||
const topLine =
|
||||
dimBorder("╭─") +
|
||||
dimBorder(titleStr) +
|
||||
dimBorder("─".repeat(fillLen)) +
|
||||
dimBorder("╮");
|
||||
|
||||
// Bottom border: ╰─...─╯
|
||||
const bottomLine =
|
||||
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
||||
|
||||
// Content rows
|
||||
const contentLines = rows.map((row) => {
|
||||
const visLen = stripAnsi(row);
|
||||
const pad = Math.max(0, inner - 1 - visLen);
|
||||
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
|
||||
});
|
||||
|
||||
return [topLine, ...contentLines, bottomLine].join("\n");
|
||||
}
|
||||
|
||||
// ── Format an option term (short + long) ────────────────────────────────
|
||||
|
||||
function formatOptionTerm(opt: Option): string {
|
||||
const parts: string[] = [];
|
||||
if (opt.short) parts.push(opt.short);
|
||||
if (opt.long) parts.push(opt.long);
|
||||
let term = parts.join(", ");
|
||||
|
||||
// Append value placeholder for non-boolean options
|
||||
if (opt.flags) {
|
||||
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
||||
if (match) {
|
||||
term += ` ${match[0]}`;
|
||||
}
|
||||
}
|
||||
return term;
|
||||
}
|
||||
|
||||
// ── Get the long flag name for panel lookup ─────────────────────────────
|
||||
|
||||
function getLongFlag(opt: Option): string {
|
||||
if (opt.long) return opt.long;
|
||||
return opt.short || "";
|
||||
}
|
||||
|
||||
// ── Format a default value ──────────────────────────────────────────────
|
||||
|
||||
function formatDefault(opt: Option): string {
|
||||
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
|
||||
return dim(` [default: ${opt.defaultValue}]`);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// ── The main help formatter ─────────────────────────────────────────────
|
||||
|
||||
export function richFormatHelp(cmd: Command, helper: Help): string {
|
||||
const width = process.stdout.columns || 80;
|
||||
const lines: string[] = [];
|
||||
|
||||
const isRoot = !cmd.parent;
|
||||
|
||||
// ── Usage line ──
|
||||
const usage = helper.commandUsage(cmd);
|
||||
lines.push("");
|
||||
if (isRoot) {
|
||||
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
||||
lines.push(
|
||||
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
|
||||
);
|
||||
} else {
|
||||
// Subcommands: split into command path (bold) and args (yellow)
|
||||
const usageParts = usage.split(" ");
|
||||
const cmdPath: string[] = [];
|
||||
const argParts: string[] = [];
|
||||
let pastCmd = false;
|
||||
for (const part of usageParts) {
|
||||
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
|
||||
cmdPath.push(part);
|
||||
} else {
|
||||
pastCmd = true;
|
||||
argParts.push(part);
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
// ── Description ──
|
||||
const desc = helper.commandDescription(cmd);
|
||||
if (desc) {
|
||||
// Split multi-line descriptions (e.g., title + tagline)
|
||||
const descLines = desc.split("\n");
|
||||
for (let i = 0; i < descLines.length; i++) {
|
||||
const dLine = descLines[i];
|
||||
// First line is the title, subsequent non-empty lines are tagline (dimmed)
|
||||
if (i === 0 || dLine.trim() === "") {
|
||||
lines.push(` ${dLine}`);
|
||||
} else {
|
||||
lines.push(` ${dim(dLine)}`);
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
// ── Arguments panel (subcommands only) ──
|
||||
if (!isRoot) {
|
||||
const visibleArgs = helper.visibleArguments(cmd);
|
||||
if (visibleArgs.length > 0) {
|
||||
const maxLen = Math.max(
|
||||
...visibleArgs.map((a: Argument) => a.name().length),
|
||||
);
|
||||
const argRows = visibleArgs.map((a: Argument) => {
|
||||
const name = cyanBold(a.name().padEnd(maxLen));
|
||||
const description = helper.argumentDescription(a);
|
||||
return ` ${name} ${description}`;
|
||||
});
|
||||
const panel = renderPanel("Arguments", argRows, width);
|
||||
if (panel) lines.push(panel);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Collect options (grouped into panels for subcommands) ──
|
||||
const visibleOpts = helper.visibleOptions(cmd);
|
||||
const cmdName = cmd.name();
|
||||
const panelMap =
|
||||
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
|
||||
|
||||
const grouped: Record<string, Option[]> = { Options: [] };
|
||||
for (const panelName of PANEL_ORDER) {
|
||||
grouped[panelName] = [];
|
||||
}
|
||||
|
||||
for (const opt of visibleOpts) {
|
||||
const flag = getLongFlag(opt);
|
||||
const panel = panelMap[flag];
|
||||
if (panel && PANEL_ORDER.includes(panel)) {
|
||||
grouped[panel].push(opt);
|
||||
} else {
|
||||
grouped.Options.push(opt);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Collect commands ──
|
||||
const visibleCmds = helper.visibleCommands(cmd);
|
||||
|
||||
if (isRoot) {
|
||||
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
||||
if (grouped.Options.length > 0) {
|
||||
const optRows = formatOptionRows(grouped.Options);
|
||||
const panel = renderPanel("Options", optRows, width);
|
||||
if (panel) lines.push(panel);
|
||||
}
|
||||
if (visibleCmds.length > 0) {
|
||||
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
|
||||
for (const group of COMMAND_GROUPS) {
|
||||
const groupCmds = group.commands
|
||||
.map((name) => cmdMap.get(name))
|
||||
.filter((c): c is Command => c !== undefined);
|
||||
if (groupCmds.length === 0) continue;
|
||||
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
|
||||
const cmdRows = groupCmds.map((c) => {
|
||||
const name = cyanBold(c.name().padEnd(maxLen));
|
||||
const description = helper.subcommandDescription(c);
|
||||
return ` ${name} ${description}`;
|
||||
});
|
||||
const panel = renderPanel(group.panel, cmdRows, width);
|
||||
if (panel) lines.push(panel);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// SUBCOMMANDS: Options/panels first, then sub-subcommands
|
||||
const panelSequence = ["Options", ...PANEL_ORDER];
|
||||
for (const panelName of panelSequence) {
|
||||
const opts = grouped[panelName];
|
||||
if (opts && opts.length > 0) {
|
||||
const optRows = formatOptionRows(opts);
|
||||
const panel = renderPanel(panelName, optRows, width);
|
||||
if (panel) lines.push(panel);
|
||||
}
|
||||
}
|
||||
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
|
||||
if (visibleCmds.length > 0) {
|
||||
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
|
||||
const cmdRows = visibleCmds.map((c) => {
|
||||
const name = cyanBold(c.name().padEnd(maxLen));
|
||||
const description = helper.subcommandDescription(c);
|
||||
return ` ${name} ${description}`;
|
||||
});
|
||||
const panel = renderPanel("Commands", cmdRows, width);
|
||||
if (panel) lines.push(panel);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ── Format option rows with aligned columns ─────────────────────────────
|
||||
|
||||
function formatOptionRows(opts: Option[]): string[] {
|
||||
const terms = opts.map((o) => formatOptionTerm(o));
|
||||
const maxTermLen = Math.max(...terms.map((t) => t.length));
|
||||
|
||||
return opts.map((opt, i) => {
|
||||
const term = cyanBold(terms[i].padEnd(maxTermLen));
|
||||
const desc = opt.description || "";
|
||||
const def = formatDefault(opt);
|
||||
return ` ${term} ${desc}${def}`;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
|
||||
|
||||
function sortCommands(cmds: Command[]): Command[] {
|
||||
return [...cmds].sort((a, b) => {
|
||||
const ai = COMMAND_ORDER.indexOf(a.name());
|
||||
const bi = COMMAND_ORDER.indexOf(b.name());
|
||||
// Unknown commands go to end, preserving original order
|
||||
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
|
||||
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
|
||||
return aIdx - bIdx;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,838 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Main CLI application — the entrypoint for `mem0`.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Command } from "commander";
|
||||
import { AuthError, type Backend, getBackend } from "./backend/index.js";
|
||||
import { colors, printError, printWarning } from "./branding.js";
|
||||
import type { Mem0Config } from "./config.js";
|
||||
import { loadConfig, saveConfig } from "./config.js";
|
||||
import { richFormatHelp } from "./help.js";
|
||||
import {
|
||||
isAgentMode,
|
||||
setAgentMode,
|
||||
setCurrentCommand,
|
||||
takeNotice,
|
||||
} from "./state.js";
|
||||
import { captureEvent } from "./telemetry.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
// ── Validated user identity (set by getBackendAndConfig) ─────────────────
|
||||
|
||||
let _validatedUserEmail: string | undefined;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
async function getBackendAndConfig(
|
||||
apiKey?: string,
|
||||
baseUrl?: string,
|
||||
): Promise<{ backend: Backend; config: Mem0Config }> {
|
||||
const config = loadConfig();
|
||||
|
||||
if (apiKey) config.platform.apiKey = apiKey;
|
||||
if (baseUrl) config.platform.baseUrl = baseUrl;
|
||||
|
||||
if (!config.platform.apiKey) {
|
||||
printError(
|
||||
"No API key configured.",
|
||||
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const backend = getBackend(config);
|
||||
|
||||
// Validate the API key upfront with a fast timeout
|
||||
try {
|
||||
const pingData = (await Promise.race([
|
||||
backend.ping(),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("timeout")), 5000),
|
||||
),
|
||||
])) as Record<string, unknown>;
|
||||
|
||||
const email = pingData?.user_email as string | undefined;
|
||||
if (email) {
|
||||
_validatedUserEmail = email;
|
||||
if (config.platform.userEmail !== email) {
|
||||
config.platform.userEmail = email;
|
||||
try {
|
||||
saveConfig(config);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) {
|
||||
printError(
|
||||
"Invalid or expired API key.",
|
||||
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// Network error / timeout — warn but proceed
|
||||
printWarning(
|
||||
"Could not validate API key (network issue). Proceeding anyway.",
|
||||
);
|
||||
}
|
||||
|
||||
return { backend, config };
|
||||
}
|
||||
|
||||
async function getBackendOnly(
|
||||
apiKey?: string,
|
||||
baseUrl?: string,
|
||||
): Promise<Backend> {
|
||||
return (await getBackendAndConfig(apiKey, baseUrl)).backend;
|
||||
}
|
||||
|
||||
function checkAgentMode(): boolean {
|
||||
const rootOpts = program.opts();
|
||||
const isAgent = !!(rootOpts.json || rootOpts.agent);
|
||||
if (isAgent) setAgentMode(true);
|
||||
return isAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve entity IDs: CLI flag > config default > undefined.
|
||||
*
|
||||
* If any explicit ID is provided, only use explicit IDs (don't mix
|
||||
* in defaults for other entity types which would over-filter).
|
||||
* If no explicit IDs, fall back to all configured defaults.
|
||||
*/
|
||||
function resolveIds(
|
||||
config: Mem0Config,
|
||||
opts: {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
},
|
||||
): { userId?: string; agentId?: string; appId?: string; runId?: string } {
|
||||
const hasExplicit = !!(
|
||||
opts.userId ||
|
||||
opts.agentId ||
|
||||
opts.appId ||
|
||||
opts.runId
|
||||
);
|
||||
if (hasExplicit) {
|
||||
return {
|
||||
userId: opts.userId || undefined,
|
||||
agentId: opts.agentId || undefined,
|
||||
appId: opts.appId || undefined,
|
||||
runId: opts.runId || undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
userId: config.defaults.userId || undefined,
|
||||
agentId: config.defaults.agentId || undefined,
|
||||
appId: config.defaults.appId || undefined,
|
||||
runId: config.defaults.runId || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Main program ──────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.name("mem0")
|
||||
.description(
|
||||
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
|
||||
)
|
||||
// Positional options: flags AFTER a subcommand name belong to that
|
||||
// subcommand, not the global program. Without this, `mem0 init --agent`
|
||||
// routes `--agent` to the program-level alias (for --json) and init's own
|
||||
// `--agent` (Agent Mode bootstrap) silently never fires.
|
||||
.enablePositionalOptions()
|
||||
.option("--version", "Show version and exit.")
|
||||
.on("option:version", () => {
|
||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
process.exit(0);
|
||||
})
|
||||
.option("--json", "Output as JSON for agent/programmatic use.")
|
||||
.option(
|
||||
"--agent",
|
||||
"Output as JSON for agent/programmatic use. (alias: --json) Place BEFORE the subcommand: `mem0 --agent <cmd>`. On `init`, `mem0 init --agent` is the Agent Mode bootstrap flag instead.",
|
||||
)
|
||||
.usage("<command> [options]")
|
||||
.helpOption("--help", "Show this message and exit.")
|
||||
.addHelpCommand(false)
|
||||
.configureHelp({ formatHelp: richFormatHelp });
|
||||
|
||||
// ── Telemetry hook ───────────────────────────────────────────────────────
|
||||
|
||||
program.hook("preAction", (_thisCommand, actionCommand) => {
|
||||
try {
|
||||
const commandName = actionCommand.name();
|
||||
const parentName = actionCommand.parent?.name();
|
||||
const fullCommand =
|
||||
parentName && parentName !== "mem0"
|
||||
? `${parentName}.${commandName}`
|
||||
: commandName;
|
||||
// Stash the active command name in shared state so the JSON
|
||||
// error envelope (printError) can report which command failed
|
||||
// instead of an empty `"command": ""` field.
|
||||
setCurrentCommand(fullCommand);
|
||||
// init fires its own telemetry from runInit with full M1-M6 props
|
||||
// (mode/agent_caller/signup_source/claimed_agent_mode); skip the
|
||||
// auto-fire here so we don't double-count.
|
||||
if (fullCommand === "init") return;
|
||||
const isAgent = !!(program.opts().json || program.opts().agent);
|
||||
captureEvent(
|
||||
`cli.${fullCommand}`,
|
||||
{
|
||||
command: fullCommand,
|
||||
is_agent: isAgent,
|
||||
},
|
||||
_validatedUserEmail,
|
||||
);
|
||||
} catch {
|
||||
/* silently swallow */
|
||||
}
|
||||
});
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("init")
|
||||
.description("Interactive setup wizard for mem0 CLI.")
|
||||
.option("--api-key <key>", "API key (skip prompt).")
|
||||
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
|
||||
.option("--email <email>", "Login via email verification code.")
|
||||
.option(
|
||||
"--code <code>",
|
||||
"Verification code (use with --email for non-interactive login).",
|
||||
)
|
||||
.option("--force", "Overwrite existing config without confirmation.", false)
|
||||
.option(
|
||||
"--agent",
|
||||
"Bootstrap an unattended Agent Mode account (no email required).",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--source <channel>",
|
||||
"Channel attribution for signup (e.g. github, hn, ph).",
|
||||
)
|
||||
.option(
|
||||
"--agent-caller <name>",
|
||||
"Self-declared agent identity (e.g. claude-code, cursor). Used with --agent to attribute Agent Mode signups.",
|
||||
)
|
||||
// Accept `--json` at the init level too so the PRD-documented form
|
||||
// `mem0 init --agent --json` works without requiring users to move it
|
||||
// before the subcommand. Effect is identical to the global `--json`:
|
||||
// flip agent-mode output state.
|
||||
.option("--json", "Output as JSON (alias for global `--json`).", false)
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456\n $ mem0 init --agent # Bootstrap an Agent Mode account (unattended)\n $ mem0 init --email you@example.com # Claims an existing Agent Mode key when one is present",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
// `--json` at init level mirrors the global flag — flip agent_mode
|
||||
// state so downstream formatters use JSON envelopes.
|
||||
if (opts.json) setAgentMode(true);
|
||||
const { runInit } = await import("./commands/init.js");
|
||||
await runInit({
|
||||
apiKey: opts.apiKey,
|
||||
userId: opts.userId,
|
||||
email: opts.email,
|
||||
code: opts.code,
|
||||
force: opts.force,
|
||||
agent: opts.agent,
|
||||
source: opts.source,
|
||||
agentCaller: opts.agentCaller,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Setup: identify (post-bootstrap agent self-tag) ──────────────────────
|
||||
|
||||
program
|
||||
.command("identify <name>")
|
||||
.description(
|
||||
"Tag your active Agent Mode key with the AI agent that's using it (e.g. claude-code, cursor).",
|
||||
)
|
||||
.action(async (name: string) => {
|
||||
const { runIdentify } = await import("./commands/identify.js");
|
||||
await runIdentify(name);
|
||||
});
|
||||
|
||||
// ── Memory: add ───────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("add [text]")
|
||||
.description("Add a memory from text, messages, file, or stdin.")
|
||||
.option("-u, --user-id <id>", "Scope to user.")
|
||||
.option("--agent-id <id>", "Scope to agent.")
|
||||
.option("--app-id <id>", "Scope to app.")
|
||||
.option("--run-id <id>", "Scope to run.")
|
||||
.option("--messages <json>", "Conversation messages as JSON.")
|
||||
.option("-f, --file <path>", "Read messages from JSON file.")
|
||||
.option("-m, --metadata <json>", "Custom metadata as JSON.")
|
||||
.option("--immutable", "Prevent future updates.", false)
|
||||
.option("--no-infer", "Skip inference, store raw.")
|
||||
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
|
||||
.option("--categories <value>", "Categories (JSON array or comma-separated).")
|
||||
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
'\nExamples:\n $ mem0 add "I prefer dark mode" --user-id alice\n $ echo "text" | mem0 add -u alice\n $ mem0 add --file msgs.json -u alice -o json',
|
||||
)
|
||||
.action(async (text, opts) => {
|
||||
const { cmdAdd } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const ids = resolveIds(config, opts);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdAdd(backend, text, { ...ids, ...opts, output });
|
||||
});
|
||||
|
||||
// ── Memory: search ────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("search [query]")
|
||||
.description(
|
||||
"Query your memory store — semantic, keyword, or hybrid retrieval.",
|
||||
)
|
||||
.option("-u, --user-id <id>", "Filter by user.")
|
||||
.option("--agent-id <id>", "Filter by agent.")
|
||||
.option("--app-id <id>", "Filter by app.")
|
||||
.option("--run-id <id>", "Filter by run.")
|
||||
.option(
|
||||
"-k, --top-k <n>",
|
||||
"Number of results.",
|
||||
(v) => Number.parseInt(v),
|
||||
10,
|
||||
)
|
||||
.option(
|
||||
"--threshold <n>",
|
||||
"Minimum similarity score.",
|
||||
(v) => Number.parseFloat(v),
|
||||
0.3,
|
||||
)
|
||||
.option("--rerank", "Enable reranking (Platform only).", false)
|
||||
.option("--keyword", "Use keyword search.", false)
|
||||
.option("--filter <json>", "Advanced filter expression (JSON).")
|
||||
.option("--fields <list>", "Specific fields to return (comma-separated).")
|
||||
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
'\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice',
|
||||
)
|
||||
.action(async (query, opts) => {
|
||||
let resolvedQuery = query;
|
||||
if (!resolvedQuery && !process.stdin.isTTY) {
|
||||
resolvedQuery = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
if (!resolvedQuery) {
|
||||
printError("No query provided. Pass a query argument or pipe via stdin.");
|
||||
process.exit(1);
|
||||
}
|
||||
const { cmdSearch } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const ids = resolveIds(config, opts);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdSearch(backend, resolvedQuery, {
|
||||
...ids,
|
||||
topK: opts.topK,
|
||||
threshold: opts.threshold,
|
||||
rerank: opts.rerank,
|
||||
keyword: opts.keyword,
|
||||
filterJson: opts.filter,
|
||||
fields: opts.fields,
|
||||
output,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Memory: get ───────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("get <memoryId>")
|
||||
.description("Get a specific memory by ID.")
|
||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json",
|
||||
)
|
||||
.action(async (memoryId, opts) => {
|
||||
const { cmdGet } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdGet(backend, memoryId, { output });
|
||||
});
|
||||
|
||||
// ── Memory: list ──────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("list")
|
||||
.description("List memories with optional filters.")
|
||||
.option("-u, --user-id <id>", "Filter by user.")
|
||||
.option("--agent-id <id>", "Filter by agent.")
|
||||
.option("--app-id <id>", "Filter by app.")
|
||||
.option("--run-id <id>", "Filter by run.")
|
||||
.option("--page <n>", "Page number.", (v) => Number.parseInt(v), 1)
|
||||
.option(
|
||||
"--page-size <n>",
|
||||
"Results per page.",
|
||||
(v) => Number.parseInt(v),
|
||||
100,
|
||||
)
|
||||
.option("--category <name>", "Filter by category.")
|
||||
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
||||
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
||||
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const { cmdList } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const ids = resolveIds(config, opts);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdList(backend, {
|
||||
...ids,
|
||||
page: opts.page,
|
||||
pageSize: opts.pageSize,
|
||||
category: opts.category,
|
||||
after: opts.after,
|
||||
before: opts.before,
|
||||
output,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Memory: update ────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("update <memoryId> [text]")
|
||||
.description("Update a memory's text or metadata.")
|
||||
.option("-m, --metadata <json>", "Update metadata (JSON).")
|
||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
`\nExamples:\n $ mem0 update abc-123 "new text"\n $ mem0 update abc-123 --metadata '{"key":"val"}'\n $ echo "new text" | mem0 update abc-123`,
|
||||
)
|
||||
.action(async (memoryId, text, opts) => {
|
||||
let resolvedText = text;
|
||||
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
|
||||
resolvedText = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
const { cmdUpdate } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdUpdate(backend, memoryId, resolvedText, {
|
||||
metadata: opts.metadata,
|
||||
output,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Memory: delete (consolidated) ─────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("delete [memoryId]")
|
||||
.description("Delete a memory, all memories matching a scope, or an entity.")
|
||||
.option("--all", "Delete all memories matching scope filters.", false)
|
||||
.option(
|
||||
"--entity",
|
||||
"Delete the entity itself and all its memories (cascade).",
|
||||
false,
|
||||
)
|
||||
.option("--project", "With --all: delete ALL memories project-wide.", false)
|
||||
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
||||
.option("--force", "Skip confirmation.", false)
|
||||
.option("-u, --user-id <id>", "Scope to user.")
|
||||
.option("--agent-id <id>", "Scope to agent.")
|
||||
.option("--app-id <id>", "Scope to app.")
|
||||
.option("--run-id <id>", "Scope to run.")
|
||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
[
|
||||
"\nExamples:",
|
||||
" $ mem0 delete abc-123-def-456 # single memory",
|
||||
" $ mem0 delete --all -u alice --force # all memories for user",
|
||||
" $ mem0 delete --all --project --force # project-wide wipe",
|
||||
" $ mem0 delete --entity -u alice --force # entity + all its memories",
|
||||
].join("\n"),
|
||||
)
|
||||
.action(async (memoryId, opts) => {
|
||||
const isAgent = checkAgentMode();
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
// ── Mutual-exclusion checks ──
|
||||
if (memoryId && opts.all) {
|
||||
printError("Cannot combine <memoryId> with --all. Use one or the other.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (memoryId && opts.entity) {
|
||||
printError(
|
||||
"Cannot combine <memoryId> with --entity. Use one or the other.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (opts.all && opts.entity) {
|
||||
printError("Cannot combine --all with --entity. Use one or the other.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!memoryId && !opts.all && !opts.entity) {
|
||||
printError(
|
||||
"Specify a memory ID, --all, or --entity.\n" +
|
||||
" mem0 delete <id> Delete a single memory\n" +
|
||||
" mem0 delete --all [scope] Delete all memories matching scope\n" +
|
||||
" mem0 delete --entity [scope] Delete an entity and all its memories",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Dispatch: single memory ──
|
||||
if (memoryId) {
|
||||
const { cmdDelete } = await import("./commands/memory.js");
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdDelete(backend, memoryId, {
|
||||
output,
|
||||
dryRun: opts.dryRun,
|
||||
force: opts.force,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Dispatch: --all ──
|
||||
if (opts.all) {
|
||||
const { cmdDeleteAll } = await import("./commands/memory.js");
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const ids = opts.project
|
||||
? {
|
||||
userId: undefined,
|
||||
agentId: undefined,
|
||||
appId: undefined,
|
||||
runId: undefined,
|
||||
}
|
||||
: resolveIds(config, opts);
|
||||
await cmdDeleteAll(backend, {
|
||||
force: opts.force,
|
||||
dryRun: opts.dryRun,
|
||||
all: opts.project,
|
||||
...ids,
|
||||
output,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Dispatch: --entity ──
|
||||
if (opts.entity) {
|
||||
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdEntitiesDelete(backend, { ...opts, output });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Config subcommands ────────────────────────────────────────────────────
|
||||
|
||||
const configCmd = program
|
||||
.command("config")
|
||||
.description("Manage mem0 configuration.")
|
||||
.addHelpCommand(false);
|
||||
|
||||
configCmd
|
||||
.command("show")
|
||||
.description("Display current configuration (secrets redacted).")
|
||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 config show\n $ mem0 config show -o json",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const { cmdConfigShow } = await import("./commands/config.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
cmdConfigShow({ output });
|
||||
});
|
||||
|
||||
configCmd
|
||||
.command("get <key>")
|
||||
.description("Get a configuration value.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id",
|
||||
)
|
||||
.action(async (key) => {
|
||||
const { cmdConfigGet } = await import("./commands/config.js");
|
||||
checkAgentMode();
|
||||
cmdConfigGet(key);
|
||||
});
|
||||
|
||||
configCmd
|
||||
.command("set <key> <value>")
|
||||
.description("Set a configuration value.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 config set defaults.user_id alice\n $ mem0 config set platform.base_url https://api.mem0.ai",
|
||||
)
|
||||
.action(async (key, value) => {
|
||||
const { cmdConfigSet } = await import("./commands/config.js");
|
||||
checkAgentMode();
|
||||
cmdConfigSet(key, value);
|
||||
});
|
||||
|
||||
// ── Entity subcommand group ───────────────────────────────────────────────
|
||||
|
||||
const entityCmd = program
|
||||
.command("entity")
|
||||
.description("Manage entities.")
|
||||
.addHelpCommand(false)
|
||||
.configureHelp({ formatHelp: richFormatHelp });
|
||||
|
||||
entityCmd
|
||||
.command("list <entityType>")
|
||||
.description("List all entities of a given type.")
|
||||
.option("-o, --output <format>", "Output: table, json.", "table")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json",
|
||||
)
|
||||
.action(async (entityType, opts) => {
|
||||
const { cmdEntitiesList } = await import("./commands/entities.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdEntitiesList(backend, entityType, { output });
|
||||
});
|
||||
|
||||
entityCmd
|
||||
.command("delete")
|
||||
.description("Delete an entity and ALL its memories (cascade).")
|
||||
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
||||
.option("-u, --user-id <id>", "Scope to user.")
|
||||
.option("--agent-id <id>", "Scope to agent.")
|
||||
.option("--app-id <id>", "Scope to app.")
|
||||
.option("--run-id <id>", "Scope to run.")
|
||||
.option("--force", "Skip confirmation.", false)
|
||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdEntitiesDelete(backend, { ...opts, output });
|
||||
});
|
||||
|
||||
// ── Event subcommands ─────────────────────────────────────────────────────
|
||||
|
||||
const eventCmd = program
|
||||
.command("event")
|
||||
.description("Inspect background processing events.")
|
||||
.addHelpCommand(false)
|
||||
.configureHelp({ formatHelp: richFormatHelp });
|
||||
|
||||
eventCmd
|
||||
.command("list")
|
||||
.description("List recent background processing events.")
|
||||
.option("-o, --output <format>", "Output: table, json.", "table")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 event list\n $ mem0 event list -o json",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const { cmdEventList } = await import("./commands/events.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdEventList(backend, { output });
|
||||
});
|
||||
|
||||
eventCmd
|
||||
.command("status <eventId>")
|
||||
.description("Check the status of a specific background event.")
|
||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 event status <event-id>\n $ mem0 event status <event-id> -o json",
|
||||
)
|
||||
.action(async (eventId, opts) => {
|
||||
const { cmdEventStatus } = await import("./commands/events.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdEventStatus(backend, eventId, { output });
|
||||
});
|
||||
|
||||
// ── Utility commands ──────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("status")
|
||||
.description("Check connectivity and authentication.")
|
||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText("after", "\nExamples:\n $ mem0 status\n $ mem0 status -o json")
|
||||
.action(async (opts) => {
|
||||
const { cmdStatus } = await import("./commands/utils.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdStatus(backend, {
|
||||
userId: config.defaults.userId || undefined,
|
||||
agentId: config.defaults.agentId || undefined,
|
||||
output,
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command("import <filePath>")
|
||||
.description("Import memories from a JSON file.")
|
||||
.option("-u, --user-id <id>", "Override user ID.")
|
||||
.option("--agent-id <id>", "Override agent ID.")
|
||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json",
|
||||
)
|
||||
.action(async (filePath, opts) => {
|
||||
const { cmdImport } = await import("./commands/utils.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const ids = resolveIds(config, opts);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdImport(backend, filePath, {
|
||||
userId: ids.userId,
|
||||
agentId: ids.agentId,
|
||||
output,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Help (machine-readable) ──────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("help")
|
||||
.description(
|
||||
"Show help. Use --json for machine-readable output (for LLM agents).",
|
||||
)
|
||||
.option("--json", "Output machine-readable JSON for LLM agents.", false)
|
||||
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
||||
.action((opts) => {
|
||||
// opts.json is set when `mem0 help --json` is used (subcommand flag).
|
||||
// program.opts().json is set when the root --json global flag was used first.
|
||||
if (opts.json || program.opts().json) {
|
||||
// Load spec from parent directory
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
|
||||
if (fs.existsSync(specPath)) {
|
||||
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
|
||||
console.log(JSON.stringify(spec, null, 2));
|
||||
} else {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "mem0",
|
||||
version: CLI_VERSION,
|
||||
description: "The Memory Layer for AI Agents",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const { brand: b } = colors;
|
||||
console.log(
|
||||
`${b("◆ Mem0 CLI")} v${CLI_VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`,
|
||||
);
|
||||
console.log("Usage: mem0 <command> [OPTIONS]\n");
|
||||
console.log("Commands:");
|
||||
console.log(
|
||||
" add Add a memory from text, messages, file, or stdin",
|
||||
);
|
||||
console.log(
|
||||
" search Query your memory store (semantic, keyword, hybrid)",
|
||||
);
|
||||
console.log(" get Get a specific memory by ID");
|
||||
console.log(" list List memories with optional filters");
|
||||
console.log(" update Update a memory's text or metadata");
|
||||
console.log(
|
||||
" delete Delete a memory, all memories, or an entity",
|
||||
);
|
||||
console.log(" import Import memories from a JSON file");
|
||||
console.log(" config Manage configuration (show, get, set)");
|
||||
console.log(" entity Manage entities (list, delete)");
|
||||
console.log(
|
||||
" event Inspect background events (list, status)",
|
||||
);
|
||||
console.log(" init Interactive setup wizard");
|
||||
console.log(" status Check connectivity and authentication");
|
||||
console.log();
|
||||
console.log(" mem0 <command> --help Get help for a command");
|
||||
console.log(
|
||||
" mem0 help --json Machine-readable help (for LLM agents)",
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||
|
||||
// Surface any unclaimed Agent Mode notice once per command, after the primary
|
||||
// output. In JSON/agent mode the notice is folded into the envelope by
|
||||
// formatJsonEnvelope, so skip the stderr banner there to avoid duplication.
|
||||
function surfaceNotice(): void {
|
||||
const notice = takeNotice();
|
||||
if (notice && !isAgentMode()) {
|
||||
process.stderr.write(`\n\x1b[33m🔔 ${notice}\x1b[0m\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
program.parseAsync().finally(() => {
|
||||
surfaceNotice();
|
||||
});
|
||||
@@ -0,0 +1,397 @@
|
||||
/**
|
||||
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
||||
*/
|
||||
|
||||
import boxen from "boxen";
|
||||
import Table from "cli-table3";
|
||||
import { colors, sym } from "./branding.js";
|
||||
import { takeNotice } from "./state.js";
|
||||
|
||||
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||
|
||||
function formatDate(dtStr?: string): string | undefined {
|
||||
if (!dtStr) return undefined;
|
||||
try {
|
||||
const dt = new Date(dtStr.replace("Z", "+00:00"));
|
||||
return dt.toISOString().slice(0, 10);
|
||||
} catch {
|
||||
return dtStr?.slice(0, 10);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMemoriesText(
|
||||
memories: Record<string, unknown>[],
|
||||
title = "memories",
|
||||
): void {
|
||||
const count = memories.length;
|
||||
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
|
||||
|
||||
for (let i = 0; i < memories.length; i++) {
|
||||
const mem = memories[i];
|
||||
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
||||
const score = mem.score as number | undefined;
|
||||
const created = formatDate(mem.created_at as string | undefined);
|
||||
let category: string | undefined;
|
||||
const cats = mem.categories;
|
||||
if (Array.isArray(cats)) {
|
||||
category = cats[0] as string | undefined;
|
||||
}
|
||||
|
||||
console.log(` ${i + 1}. ${memoryText}`);
|
||||
|
||||
const details: string[] = [];
|
||||
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
|
||||
if (memId) details.push(`ID: ${memId}`);
|
||||
if (created) details.push(`Created: ${created}`);
|
||||
if (category) details.push(`Category: ${category}`);
|
||||
|
||||
if (details.length > 0) {
|
||||
console.log(` ${dim(details.join(" · "))}`);
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
export function formatMemoriesTable(
|
||||
memories: Record<string, unknown>[],
|
||||
opts: { showScore?: boolean } = {},
|
||||
): void {
|
||||
const head = opts.showScore
|
||||
? [
|
||||
accent("ID"),
|
||||
accent("Score"),
|
||||
accent("Memory"),
|
||||
accent("Category"),
|
||||
accent("Created"),
|
||||
]
|
||||
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
|
||||
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
|
||||
const table = new Table({
|
||||
head,
|
||||
colWidths,
|
||||
wordWrap: true,
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const mem of memories) {
|
||||
const memId = (mem.id as string) ?? "";
|
||||
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||
if (memoryText.length > 60) {
|
||||
memoryText = `${memoryText.slice(0, 57)}...`;
|
||||
}
|
||||
const categories = mem.categories;
|
||||
const cat =
|
||||
Array.isArray(categories) && categories.length > 0
|
||||
? categories.length > 1
|
||||
? `${categories[0]} (+${categories.length - 1})`
|
||||
: (categories[0] as string)
|
||||
: "—";
|
||||
const created = formatDate(mem.created_at as string | undefined) ?? "—";
|
||||
if (opts.showScore) {
|
||||
const score = mem.score as number | undefined;
|
||||
const scoreStr = score !== undefined ? score.toFixed(2) : "—";
|
||||
table.push([dim(memId), scoreStr, memoryText, cat, created]);
|
||||
} else {
|
||||
table.push([dim(memId), memoryText, cat, created]);
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(table.toString());
|
||||
console.log();
|
||||
}
|
||||
|
||||
export function formatJson(data: unknown): void {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function formatSingleMemory(
|
||||
mem: Record<string, unknown>,
|
||||
output = "text",
|
||||
): void {
|
||||
if (output === "json") {
|
||||
formatJson(mem);
|
||||
return;
|
||||
}
|
||||
|
||||
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||
const memId = (mem.id ?? "") as string;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(` ${memoryText}`);
|
||||
lines.push("");
|
||||
|
||||
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
|
||||
const created = formatDate(mem.created_at as string | undefined);
|
||||
if (created) lines.push(` ${dim("Created:")} ${created}`);
|
||||
const updated = formatDate(mem.updated_at as string | undefined);
|
||||
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
|
||||
const meta = mem.metadata;
|
||||
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
||||
const categories = mem.categories;
|
||||
if (categories) {
|
||||
const catStr = Array.isArray(categories)
|
||||
? categories.join(", ")
|
||||
: String(categories);
|
||||
lines.push(` ${dim("Categories:")} ${catStr}`);
|
||||
}
|
||||
|
||||
const content = lines.join("\n");
|
||||
console.log();
|
||||
console.log(
|
||||
boxen(content, {
|
||||
title: brand("Memory"),
|
||||
titleAlignment: "left",
|
||||
borderColor: "magenta",
|
||||
padding: 1,
|
||||
}),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
export function formatAddResult(
|
||||
result: Record<string, unknown> | Record<string, unknown>[],
|
||||
output = "text",
|
||||
): void {
|
||||
if (output === "json") {
|
||||
formatJson(result);
|
||||
return;
|
||||
}
|
||||
if (output === "quiet") return;
|
||||
|
||||
const results: Record<string, unknown>[] = Array.isArray(result)
|
||||
? result
|
||||
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
||||
|
||||
if (!results.length) {
|
||||
console.log(` ${dim("No memories extracted.")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
const seenPendingEvents = new Set<string>();
|
||||
for (const r of results) {
|
||||
// Detect async PENDING response
|
||||
if (r.status === "PENDING") {
|
||||
const eventId = (r.event_id as string) ?? "";
|
||||
// Deduplicate PENDING entries with the same event_id
|
||||
if (eventId && seenPendingEvents.has(eventId)) continue;
|
||||
if (eventId) seenPendingEvents.add(eventId);
|
||||
const icon = accent(sym("⧗", "..."));
|
||||
const parts = [
|
||||
` ${icon} ${dim("Queued".padEnd(10))}`,
|
||||
"Processing in background",
|
||||
];
|
||||
console.log(parts.join(" "));
|
||||
if (eventId) {
|
||||
console.log(` ${dim(` event_id: ${eventId}`)}`);
|
||||
console.log(
|
||||
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const event = (r.event ?? "ADD") as string;
|
||||
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
||||
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
|
||||
0,
|
||||
8,
|
||||
);
|
||||
|
||||
let icon: string;
|
||||
let label: string;
|
||||
if (event === "ADD") {
|
||||
icon = success("+");
|
||||
label = "Added";
|
||||
} else if (event === "UPDATE") {
|
||||
icon = accent("~");
|
||||
label = "Updated";
|
||||
} else if (event === "DELETE") {
|
||||
icon = errorColor("-");
|
||||
label = "Deleted";
|
||||
} else if (event === "NOOP") {
|
||||
icon = dim("·");
|
||||
label = "No change";
|
||||
} else {
|
||||
icon = dim("?");
|
||||
label = event;
|
||||
}
|
||||
|
||||
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
|
||||
if (memory) parts.push(memory);
|
||||
if (memId) parts.push(dim(`(${memId})`));
|
||||
console.log(parts.join(" "));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
|
||||
export function formatJsonEnvelope(opts: {
|
||||
command: string;
|
||||
data: unknown;
|
||||
durationMs?: number;
|
||||
scope?: Record<string, string | undefined>;
|
||||
count?: number;
|
||||
status?: string;
|
||||
error?: string;
|
||||
}): void {
|
||||
const envelope: Record<string, unknown> = {
|
||||
status: opts.status ?? "success",
|
||||
command: opts.command,
|
||||
};
|
||||
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
||||
if (opts.scope !== undefined) envelope.scope = opts.scope;
|
||||
if (opts.count !== undefined) envelope.count = opts.count;
|
||||
if (opts.error) envelope.error = opts.error;
|
||||
envelope.data = opts.data;
|
||||
|
||||
// If the platform flagged this as an unclaimed Agent Mode account, surface
|
||||
// the notice inside the JSON envelope so an agent consuming the output
|
||||
// sees it without needing to inspect HTTP headers.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { takeNotice } = require("./state.js");
|
||||
const notice = takeNotice();
|
||||
if (notice) envelope.mem0_notice = notice;
|
||||
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
}
|
||||
|
||||
function pick(
|
||||
obj: Record<string, unknown>,
|
||||
keys: string[],
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key of keys) {
|
||||
if (key in obj) result[key] = obj[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||
if (data === null || data === undefined) return data;
|
||||
|
||||
switch (command) {
|
||||
case "add": {
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
return items.map((item) => {
|
||||
const r = item as Record<string, unknown>;
|
||||
if (r.status === "PENDING") return pick(r, ["status", "event_id"]);
|
||||
return pick(r, ["id", "memory", "event"]);
|
||||
});
|
||||
}
|
||||
case "search":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "memory", "score", "created_at", "categories"]),
|
||||
);
|
||||
case "list":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "memory", "created_at", "categories"]),
|
||||
);
|
||||
case "get": {
|
||||
const r = data as Record<string, unknown>;
|
||||
return pick(r, [
|
||||
"id",
|
||||
"memory",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"categories",
|
||||
"metadata",
|
||||
]);
|
||||
}
|
||||
case "update": {
|
||||
const r = data as Record<string, unknown>;
|
||||
return pick(r, ["id", "memory"]);
|
||||
}
|
||||
case "delete":
|
||||
case "delete-all":
|
||||
case "entity delete":
|
||||
return data;
|
||||
case "entity list":
|
||||
return (data as Record<string, unknown>[]).map((r) => ({
|
||||
name: (r.name ?? r.id) as string,
|
||||
...pick(r, ["type", "count"]),
|
||||
}));
|
||||
case "event list":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "event_type", "status", "latency", "created_at"]),
|
||||
);
|
||||
case "event status": {
|
||||
const ev = data as Record<string, unknown>;
|
||||
const rawResults =
|
||||
(ev.results as Record<string, unknown>[] | undefined) ?? [];
|
||||
const sanitizedResults = rawResults.map((r) => {
|
||||
const nested = r.data as Record<string, unknown> | undefined;
|
||||
return {
|
||||
id: r.id,
|
||||
event: r.event,
|
||||
user_id: r.user_id,
|
||||
memory: nested?.memory ?? null,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...pick(ev, [
|
||||
"id",
|
||||
"event_type",
|
||||
"status",
|
||||
"latency",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]),
|
||||
results: sanitizedResults,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatAgentEnvelope(opts: {
|
||||
command: string;
|
||||
data: unknown;
|
||||
durationMs?: number;
|
||||
scope?: Record<string, string | undefined>;
|
||||
count?: number;
|
||||
}): void {
|
||||
const envelope: Record<string, unknown> = {
|
||||
status: "success",
|
||||
command: opts.command,
|
||||
};
|
||||
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
||||
if (opts.scope) {
|
||||
const filtered = Object.fromEntries(
|
||||
Object.entries(opts.scope).filter(([, v]) => v),
|
||||
);
|
||||
if (Object.keys(filtered).length > 0) envelope.scope = filtered;
|
||||
}
|
||||
if (opts.count !== undefined) envelope.count = opts.count;
|
||||
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
||||
|
||||
// Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
||||
// agent reading the JSON output sees it without inspecting HTTP headers.
|
||||
const notice = takeNotice();
|
||||
if (notice) envelope.mem0_notice = notice;
|
||||
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
}
|
||||
|
||||
export function printResultSummary(opts: {
|
||||
count: number;
|
||||
durationSecs?: number;
|
||||
page?: number;
|
||||
scopeIds?: Record<string, string | undefined>;
|
||||
}): void {
|
||||
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
|
||||
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
|
||||
if (opts.scopeIds) {
|
||||
const scopeParts = Object.entries(opts.scopeIds)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) => `${k}=${v}`);
|
||||
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
||||
}
|
||||
if (opts.durationSecs !== undefined)
|
||||
parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
||||
|
||||
console.log(` ${dim(parts.join(" · "))}`);
|
||||
console.log();
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Sync the active Mem0 API key into other ecosystem touchpoints.
|
||||
*
|
||||
* Why: the CLI canonical state is ~/.mem0/config.json. MCP servers
|
||||
* (Claude Code plugin, Codex plugin) read MEM0_API_KEY from env or
|
||||
* their own config files. Without a sync, agent-mode bootstrap mints a
|
||||
* new key into config.json but the plugin's MCP keeps using the old
|
||||
* key from env — silent surprise.
|
||||
*
|
||||
* Design:
|
||||
* - Update ONLY entries that already exist; never create new ones
|
||||
* - Preserve surrounding content, formatting, other keys
|
||||
* - Atomic writes (tmp + rename) so a crash mid-write doesn't corrupt
|
||||
* - Idempotent — re-running with the same key is a no-op
|
||||
*
|
||||
* Targets:
|
||||
* - ~/.claude/settings.json::env::MEM0_API_KEY (Claude Code env injection)
|
||||
* - ~/.zshrc / ~/.bashrc `export MEM0_API_KEY="..."` lines
|
||||
*
|
||||
* Out of scope: Codex / Cursor MCP configs and the plugin's own
|
||||
* <plugin-dir>/.api_key file (plugin-managed, different schema).
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const CLAUDE_SETTINGS = path.join(os.homedir(), ".claude", "settings.json");
|
||||
const SHELL_RCS = [
|
||||
path.join(os.homedir(), ".zshrc"),
|
||||
path.join(os.homedir(), ".bashrc"),
|
||||
path.join(os.homedir(), ".bash_profile"),
|
||||
];
|
||||
|
||||
// Use [ \t]* (not \s*) so a trailing newline at end-of-file is preserved
|
||||
// when the MEM0_API_KEY export is the last line of the rc file.
|
||||
const RC_LINE_RE =
|
||||
/^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["']?)([^"'\n]*)(["']?)[ \t]*$/m;
|
||||
|
||||
export function syncApiKey(apiKey: string): string[] {
|
||||
if (!apiKey) return [];
|
||||
const updated: string[] = [];
|
||||
if (updateClaudeSettings(CLAUDE_SETTINGS, apiKey)) {
|
||||
updated.push(CLAUDE_SETTINGS);
|
||||
}
|
||||
for (const rc of SHELL_RCS) {
|
||||
if (updateShellRc(rc, apiKey)) updated.push(rc);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests; consumers should use {@link syncApiKey}. */
|
||||
export function updateClaudeSettings(
|
||||
filePath: string,
|
||||
apiKey: string,
|
||||
): boolean {
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
let raw: string;
|
||||
let data: Record<string, unknown>;
|
||||
try {
|
||||
raw = fs.readFileSync(filePath, "utf-8");
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const env = data.env;
|
||||
if (!env || typeof env !== "object" || !("MEM0_API_KEY" in env)) {
|
||||
return false; // no existing entry — don't create one
|
||||
}
|
||||
const envObj = env as Record<string, string>;
|
||||
if (envObj.MEM0_API_KEY === apiKey) return false; // already in sync
|
||||
envObj.MEM0_API_KEY = apiKey;
|
||||
atomicWriteText(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests; consumers should use {@link syncApiKey}. */
|
||||
export function updateShellRc(filePath: string, apiKey: string): boolean {
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
let text: string;
|
||||
try {
|
||||
text = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const match = text.match(RC_LINE_RE);
|
||||
if (!match) return false; // no existing line
|
||||
if (match[3] === apiKey) return false;
|
||||
const newText = text.replace(
|
||||
RC_LINE_RE,
|
||||
(_full, prefix) => `${prefix}"${apiKey}"`,
|
||||
);
|
||||
atomicWriteText(filePath, newText);
|
||||
return true;
|
||||
}
|
||||
|
||||
function atomicWriteText(filePath: string, content: string): void {
|
||||
const dir = path.dirname(filePath);
|
||||
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
||||
try {
|
||||
fs.writeFileSync(tmp, content, "utf-8");
|
||||
// Preserve permissions if original existed.
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const mode = fs.statSync(filePath).mode & 0o777;
|
||||
fs.chmodSync(tmp, mode);
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
fs.renameSync(tmp, filePath);
|
||||
} catch (err) {
|
||||
try {
|
||||
fs.unlinkSync(tmp);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Agent mode state — set by the root program option handler,
|
||||
* read by commands and branding functions.
|
||||
*/
|
||||
|
||||
let _agentMode = false;
|
||||
let _currentCommand = "";
|
||||
let _pendingNotice = "";
|
||||
|
||||
export function isAgentMode(): boolean {
|
||||
return _agentMode;
|
||||
}
|
||||
|
||||
export function setAgentMode(val: boolean): void {
|
||||
_agentMode = val;
|
||||
}
|
||||
|
||||
export function getCurrentCommand(): string {
|
||||
return _currentCommand;
|
||||
}
|
||||
|
||||
export function setCurrentCommand(name: string): void {
|
||||
_currentCommand = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stash a Mem0 backend notice (Agent Mode unclaimed reminder) for end-of-
|
||||
* command surfacing. Called from the platform backend after each response so
|
||||
* the notice prints once per command regardless of how many sub-requests
|
||||
* fired. Last-write-wins is fine — the message text is identical.
|
||||
*/
|
||||
export function captureNotice(notice: string | null | undefined): void {
|
||||
if (notice) _pendingNotice = notice;
|
||||
}
|
||||
|
||||
export function takeNotice(): string {
|
||||
const msg = _pendingNotice;
|
||||
_pendingNotice = "";
|
||||
return msg;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* CLI telemetry — anonymous usage tracking via PostHog.
|
||||
*
|
||||
* Sends fire-and-forget events by spawning a detached child process
|
||||
* (telemetry-sender.cjs). The parent CLI process exits immediately;
|
||||
* the child handles email resolution, caching, and the HTTP POST.
|
||||
*
|
||||
* Disable with: MEM0_TELEMETRY=false
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { CONFIG_FILE, loadConfig, saveConfig } from "./config.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SENDER_SCRIPT = path.join(__dirname, "..", "telemetry-sender.cjs");
|
||||
|
||||
function isTelemetryEnabled(): boolean {
|
||||
try {
|
||||
return process.env.MEM0_TELEMETRY !== "false";
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a persistent per-machine anonymous ID, generating one if needed.
|
||||
*
|
||||
* Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
|
||||
* repeat runs on the same machine share one PostHog identity instead of
|
||||
* collapsing into a single shared fallback string.
|
||||
*/
|
||||
function getOrCreateAnonymousId(): string {
|
||||
const config = loadConfig();
|
||||
if (config.telemetry.anonymousId) {
|
||||
return config.telemetry.anonymousId;
|
||||
}
|
||||
|
||||
const newId = `cli-anon-${randomUUID().replace(/-/g, "")}`;
|
||||
config.telemetry.anonymousId = newId;
|
||||
try {
|
||||
saveConfig(config);
|
||||
} catch {
|
||||
/* ignore persistence failure — still return the generated ID */
|
||||
}
|
||||
return newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stable anonymous identifier for the current user.
|
||||
*
|
||||
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
|
||||
* persistent per-machine anonymous ID.
|
||||
*/
|
||||
function getDistinctId(): string {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
if (config.platform.userEmail) {
|
||||
return config.platform.userEmail;
|
||||
}
|
||||
if (config.platform.apiKey) {
|
||||
return createHash("md5").update(config.platform.apiKey).digest("hex");
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
return getOrCreateAnonymousId();
|
||||
} catch {
|
||||
return `cli-anon-${randomUUID().replace(/-/g, "")}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a PostHog event (non-blocking, returns void, never throws).
|
||||
* Spawns telemetry-sender.cjs as a detached subprocess.
|
||||
*
|
||||
* When `preResolvedEmail` is provided (e.g. from an upfront ping
|
||||
* validation), it is used directly as the PostHog distinct ID and the
|
||||
* subprocess skips its own `/v1/ping/` call.
|
||||
*/
|
||||
export function captureEvent(
|
||||
eventName: string,
|
||||
properties: Record<string, unknown> = {},
|
||||
preResolvedEmail?: string,
|
||||
): void {
|
||||
if (!isTelemetryEnabled()) return;
|
||||
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const distinctId = preResolvedEmail || getDistinctId();
|
||||
|
||||
// Detect anonymous → identified transition. If a stored anonymous_id
|
||||
// exists and we just resolved to a real identity, fire a one-shot
|
||||
// $identify event so PostHog stitches the pre-signup history onto
|
||||
// the authenticated profile. Clear the stored id so we don't re-alias.
|
||||
let anonIdToAlias: string | null = null;
|
||||
if (
|
||||
distinctId &&
|
||||
!distinctId.startsWith("cli-anon-") &&
|
||||
config.telemetry.anonymousId
|
||||
) {
|
||||
anonIdToAlias = config.telemetry.anonymousId;
|
||||
config.telemetry.anonymousId = "";
|
||||
try {
|
||||
saveConfig(config);
|
||||
} catch {
|
||||
/* ignore — alias may double-fire next run, harmless */
|
||||
}
|
||||
}
|
||||
|
||||
// M4: every cli.* event carries agent_mode based on the config flag
|
||||
// (unclaimed Agent Mode key). This is the growth-doc property used to
|
||||
// join init → add → search funnels in PostHog.
|
||||
const payload = {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
distinct_id: distinctId,
|
||||
event: eventName,
|
||||
properties: {
|
||||
source: "CLI",
|
||||
language: "node",
|
||||
cli_version: CLI_VERSION,
|
||||
agent_mode: Boolean(config.platform.agentMode),
|
||||
node_version: process.version,
|
||||
os: process.platform,
|
||||
...properties,
|
||||
$process_person_profile: false,
|
||||
$lib: "posthog-node",
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
payload,
|
||||
posthogHost: POSTHOG_HOST,
|
||||
needsEmail: !distinctId || !distinctId.includes("@"),
|
||||
mem0ApiKey: config.platform.apiKey || "",
|
||||
mem0BaseUrl: config.platform.baseUrl || "https://api.mem0.ai",
|
||||
configPath: CONFIG_FILE,
|
||||
anonDistinctIdToAlias: anonIdToAlias,
|
||||
};
|
||||
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[SENDER_SCRIPT, JSON.stringify(context)],
|
||||
{ detached: true, stdio: "ignore" },
|
||||
);
|
||||
child.unref();
|
||||
} catch {
|
||||
/* silently swallow */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
// __CLI_VERSION__ is replaced at build time by tsup (see tsup.config.ts).
|
||||
// When running via tsx in dev/test mode, fall back to reading package.json.
|
||||
// typeof is safe to use on undeclared identifiers — it returns 'undefined' without throwing.
|
||||
export const CLI_VERSION: string =
|
||||
typeof __CLI_VERSION__ !== "undefined"
|
||||
? (__CLI_VERSION__ as string)
|
||||
: (createRequire(import.meta.url)("../package.json") as { version: string })
|
||||
.version;
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Standalone telemetry sender — runs as a detached child process.
|
||||
*
|
||||
* Usage: node telemetry-sender.cjs '<json context>'
|
||||
*
|
||||
* This script is spawned by telemetry.captureEvent() and runs independently
|
||||
* of the parent CLI process. It:
|
||||
*
|
||||
* 1. Resolves the user's email via /v1/ping/ if not already cached
|
||||
* 2. Caches the email in ~/.mem0/config.json for future runs
|
||||
* 3. Sends the PostHog event
|
||||
*
|
||||
* All errors are silently swallowed — this process must never produce output
|
||||
* or affect the user experience.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
|
||||
function httpsRequest(url, method, headers, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const opts = {
|
||||
hostname: u.hostname,
|
||||
path: u.pathname + u.search,
|
||||
method,
|
||||
headers,
|
||||
timeout: 10000,
|
||||
};
|
||||
const req = https.request(opts, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
reject(new Error("timeout"));
|
||||
});
|
||||
if (body) {
|
||||
req.end(body);
|
||||
} else {
|
||||
req.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveAndCacheEmail(ctx, payload) {
|
||||
try {
|
||||
const pingUrl = ctx.mem0BaseUrl.replace(/\/+$/, "") + "/v1/ping/";
|
||||
const data = await httpsRequest(pingUrl, "GET", {
|
||||
Authorization: "Token " + ctx.mem0ApiKey,
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
if (data.user_email) {
|
||||
payload.distinct_id = data.user_email;
|
||||
cacheEmail(ctx.configPath, data.user_email);
|
||||
}
|
||||
} catch {
|
||||
// silently swallow
|
||||
}
|
||||
}
|
||||
|
||||
function cacheEmail(configPath, email) {
|
||||
if (!configPath) return;
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, "utf-8");
|
||||
const cfg = JSON.parse(raw);
|
||||
if (!cfg.platform) cfg.platform = {};
|
||||
cfg.platform.user_email = email;
|
||||
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
||||
} catch {
|
||||
// silently swallow
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPosthogEvent(posthogHost, payload) {
|
||||
try {
|
||||
const body = JSON.stringify(payload);
|
||||
await httpsRequest(posthogHost, "POST", {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
}, body);
|
||||
} catch {
|
||||
// silently swallow
|
||||
}
|
||||
}
|
||||
|
||||
async function sendIdentifyEvent(ctx, payload, anonId) {
|
||||
const identifyPayload = {
|
||||
api_key: payload.api_key,
|
||||
event: "$identify",
|
||||
distinct_id: payload.distinct_id,
|
||||
properties: {
|
||||
$anon_distinct_id: anonId,
|
||||
$lib: (payload.properties && payload.properties.$lib) || "posthog-node",
|
||||
},
|
||||
};
|
||||
await sendPosthogEvent(ctx.posthogHost, identifyPayload);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ctx = JSON.parse(process.argv[2]);
|
||||
const payload = ctx.payload;
|
||||
|
||||
if (ctx.needsEmail && ctx.mem0ApiKey) {
|
||||
await resolveAndCacheEmail(ctx, payload);
|
||||
}
|
||||
|
||||
// Fire $identify *after* email resolution so PostHog links the stored
|
||||
// anonymous id directly to the final identity (email, not the api-key
|
||||
// hash). The regular event is sent next so it lands under the merged
|
||||
// profile.
|
||||
if (ctx.anonDistinctIdToAlias) {
|
||||
await sendIdentifyEvent(ctx, payload, ctx.anonDistinctIdToAlias);
|
||||
}
|
||||
|
||||
await sendPosthogEvent(ctx.posthogHost, payload);
|
||||
}
|
||||
|
||||
main().catch(() => {});
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
|
||||
*
|
||||
* Mirror of `cli/python/tests/test_agent_mode.py` — both files MUST stay
|
||||
* in sync so that the Python and Node CLIs expose an identical surface
|
||||
* for the Agent Mode entrypoint. If you add a flag here, add the same
|
||||
* assertion on the Python side (and vice versa).
|
||||
*
|
||||
* Network-bound bootstrap is covered by the platform-side E2E suite
|
||||
* (`backend/tests/e2e/test_05_agent_mode.py`); these tests only verify
|
||||
* the CLI surface that ships in the binary.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
function run(
|
||||
args: string[],
|
||||
opts: { home?: string; env?: Record<string, string> } = {},
|
||||
): { stdout: string; stderr: string; exitCode: number } {
|
||||
const env = { ...process.env };
|
||||
for (const key of Object.keys(env)) {
|
||||
if (key.startsWith("MEM0_")) delete env[key];
|
||||
}
|
||||
if (opts.home) env.HOME = opts.home;
|
||||
if (opts.env) Object.assign(env, opts.env);
|
||||
|
||||
try {
|
||||
const stdout = execSync(`npx tsx src/index.ts ${args.join(" ")}`, {
|
||||
cwd: path.join(__dirname, ".."),
|
||||
env,
|
||||
encoding: "utf-8",
|
||||
timeout: 15000,
|
||||
});
|
||||
return { stdout, stderr: "", exitCode: 0 };
|
||||
} catch (e: any) {
|
||||
return {
|
||||
stdout: e.stdout ?? "",
|
||||
stderr: e.stderr ?? "",
|
||||
exitCode: e.status ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function cleanHome(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
}
|
||||
|
||||
describe("init flag surface", () => {
|
||||
it("init --help lists --agent", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--agent");
|
||||
});
|
||||
|
||||
it("init --help describes Agent Mode", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
// Description must mention what --agent actually does so an agent
|
||||
// reading the help can self-discover the bootstrap entrypoint.
|
||||
expect(
|
||||
result.stdout.includes("Agent Mode") ||
|
||||
result.stdout.toLowerCase().includes("unattended"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("init --help lists --source", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--source");
|
||||
});
|
||||
|
||||
it("init --help lists --email and --code", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--email");
|
||||
expect(result.stdout).toContain("--code");
|
||||
});
|
||||
});
|
||||
|
||||
describe("argv preprocessing — --agent reaches init subcommand", () => {
|
||||
// Regression for the bug where the global --agent JSON-alias swallowed
|
||||
// the init-level --agent flag, making `mem0 init --agent` behave like
|
||||
// the plain interactive wizard.
|
||||
|
||||
it("init --agent triggers bootstrap branch (not the wizard)", () => {
|
||||
const home = cleanHome();
|
||||
const result = run(["init", "--agent"], {
|
||||
home,
|
||||
env: {
|
||||
MEM0_BASE_URL: "http://127.0.0.1:1", // blackhole
|
||||
FORCE_COLOR: "0",
|
||||
},
|
||||
});
|
||||
const combined = (result.stdout + result.stderr).toLowerCase();
|
||||
// Either bootstrap-attempt error, or a connection/network error —
|
||||
// both prove the --agent path executed (the wizard would prompt for
|
||||
// input and succeed/hang, not surface a network error).
|
||||
expect(
|
||||
combined.includes("agent") ||
|
||||
combined.includes("connect") ||
|
||||
combined.includes("network") ||
|
||||
combined.includes("fetch") ||
|
||||
combined.includes("bootstrap"),
|
||||
).toBe(true);
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON envelope on network failure", () => {
|
||||
it("init --agent --json does not leak a stack trace when backend is unreachable", () => {
|
||||
const home = cleanHome();
|
||||
const result = run(["init", "--agent", "--json"], {
|
||||
home,
|
||||
env: {
|
||||
MEM0_BASE_URL: "http://127.0.0.1:1",
|
||||
FORCE_COLOR: "0",
|
||||
},
|
||||
});
|
||||
const combined = result.stdout + result.stderr;
|
||||
// No raw Node stack should escape the agent-mode handler.
|
||||
expect(combined).not.toMatch(/at \w+\s*\(.+\.ts:\d+/);
|
||||
expect(combined).not.toContain("UnhandledPromiseRejection");
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("top-level help lists init", () => {
|
||||
// `mem0 --help` must list `init` so agents walking the top-level help
|
||||
// can discover the Agent Mode entrypoint without prior knowledge.
|
||||
|
||||
it("--help lists init", () => {
|
||||
const result = run(["--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("init");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Tests for branding utilities.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
BRAND_COLOR,
|
||||
SUCCESS_COLOR,
|
||||
ERROR_COLOR,
|
||||
TAGLINE,
|
||||
LOGO_MINI,
|
||||
printSuccess,
|
||||
printError,
|
||||
printWarning,
|
||||
printInfo,
|
||||
printScope,
|
||||
} from "../src/branding.js";
|
||||
|
||||
let output: string;
|
||||
let errOutput: string;
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
|
||||
beforeEach(() => {
|
||||
output = "";
|
||||
errOutput = "";
|
||||
console.log = (...args: unknown[]) => {
|
||||
output += args.map(String).join(" ") + "\n";
|
||||
};
|
||||
console.error = (...args: unknown[]) => {
|
||||
errOutput += args.map(String).join(" ") + "\n";
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.log = originalLog;
|
||||
console.error = originalError;
|
||||
});
|
||||
|
||||
describe("branding constants", () => {
|
||||
it("has correct brand color", () => {
|
||||
expect(BRAND_COLOR).toBe("#8b5cf6");
|
||||
});
|
||||
|
||||
it("has correct tagline", () => {
|
||||
expect(TAGLINE).toBe("The Memory Layer for AI Agents");
|
||||
});
|
||||
|
||||
it("has correct logo mini", () => {
|
||||
expect(LOGO_MINI).toBe("◆ mem0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printSuccess", () => {
|
||||
it("prints success message", () => {
|
||||
printSuccess("Operation completed");
|
||||
expect(output).toContain("Operation completed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printError", () => {
|
||||
it("prints error message to stderr", () => {
|
||||
printError("Something failed");
|
||||
expect(errOutput).toContain("Something failed");
|
||||
});
|
||||
|
||||
it("prints hint when provided to stderr", () => {
|
||||
printError("Failed", "Try again");
|
||||
expect(errOutput).toContain("Try again");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printWarning", () => {
|
||||
it("prints warning message to stderr", () => {
|
||||
printWarning("Be careful");
|
||||
expect(errOutput).toContain("Be careful");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printInfo", () => {
|
||||
it("prints info message", () => {
|
||||
printInfo("Important note");
|
||||
expect(errOutput).toContain("Important note");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printScope", () => {
|
||||
it("prints scope when IDs present", () => {
|
||||
printScope({ user_id: "alice", agent_id: "bot" });
|
||||
expect(errOutput).toContain("alice");
|
||||
expect(errOutput).toContain("bot");
|
||||
});
|
||||
|
||||
it("prints nothing when no IDs", () => {
|
||||
printScope({});
|
||||
expect(errOutput).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Integration tests — invoke CLI as subprocess to test end-to-end.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
function run(
|
||||
args: string[],
|
||||
opts: { home?: string; env?: Record<string, string> } = {},
|
||||
): { stdout: string; stderr: string; exitCode: number } {
|
||||
const env = { ...process.env };
|
||||
// Strip MEM0_ env vars
|
||||
for (const key of Object.keys(env)) {
|
||||
if (key.startsWith("MEM0_")) delete env[key];
|
||||
}
|
||||
if (opts.home) env.HOME = opts.home;
|
||||
if (opts.env) Object.assign(env, opts.env);
|
||||
|
||||
try {
|
||||
const stdout = execSync(
|
||||
`npx tsx src/index.ts ${args.join(" ")}`,
|
||||
{ cwd: path.join(__dirname, ".."), env, encoding: "utf-8", timeout: 15000 },
|
||||
);
|
||||
return { stdout, stderr: "", exitCode: 0 };
|
||||
} catch (e: any) {
|
||||
return {
|
||||
stdout: e.stdout ?? "",
|
||||
stderr: e.stderr ?? "",
|
||||
exitCode: e.status ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe("CLI Integration — help and version", () => {
|
||||
it("shows help with --help", () => {
|
||||
const result = run(["--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("mem0");
|
||||
expect(result.stdout).toContain("add");
|
||||
expect(result.stdout).toContain("search");
|
||||
});
|
||||
|
||||
it("help --json produces valid JSON", () => {
|
||||
const result = run(["help", "--json"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
// spec may have cli.name or top-level name
|
||||
const name = parsed.name ?? parsed.cli?.name;
|
||||
expect(name).toBe("mem0");
|
||||
});
|
||||
|
||||
it("shows add help", () => {
|
||||
const result = run(["add", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("user-id");
|
||||
expect(result.stdout).toContain("messages");
|
||||
});
|
||||
|
||||
it("shows search help", () => {
|
||||
const result = run(["search", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("top-k");
|
||||
});
|
||||
|
||||
it("shows list help", () => {
|
||||
const result = run(["list", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("page-size");
|
||||
});
|
||||
|
||||
it("shows delete help with --all, --entity, --project", () => {
|
||||
const result = run(["delete", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--all");
|
||||
expect(result.stdout).toContain("--entity");
|
||||
expect(result.stdout).toContain("--project");
|
||||
expect(result.stdout).toContain("--force");
|
||||
expect(result.stdout.toLowerCase()).toContain("memory");
|
||||
});
|
||||
|
||||
it("delete with no args errors", () => {
|
||||
const result = run(["delete"]);
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined).toContain("--all");
|
||||
});
|
||||
|
||||
it("shows entity list help", () => {
|
||||
const result = run(["entity", "list", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout.toLowerCase()).toContain("entitytype");
|
||||
});
|
||||
|
||||
it("shows entity delete help", () => {
|
||||
const result = run(["entity", "delete", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--user-id");
|
||||
expect(result.stdout).toContain("--force");
|
||||
});
|
||||
|
||||
it("shows import help", () => {
|
||||
const result = run(["import", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("add help has --output flag", () => {
|
||||
const result = run(["add", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--output");
|
||||
});
|
||||
|
||||
it("search help has --rerank flag", () => {
|
||||
const result = run(["search", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--rerank");
|
||||
});
|
||||
|
||||
it("list help has --category flag", () => {
|
||||
const result = run(["list", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--category");
|
||||
});
|
||||
});
|
||||
|
||||
describe("CLI Integration — isolated (clean home)", () => {
|
||||
function cleanHome(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
}
|
||||
|
||||
it("add without API key errors", () => {
|
||||
const home = cleanHome();
|
||||
const result = run(["add", "test", "--user-id", "alice"], { home });
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
const combined = result.stdout + result.stderr;
|
||||
expect(combined.toLowerCase()).toMatch(/api.key|error/i);
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("config show works with clean home", () => {
|
||||
const home = cleanHome();
|
||||
const result = run(["config", "show"], { home });
|
||||
expect(result.exitCode).toBe(0);
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* Tests for CLI commands using mock backend.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { createMockBackend } from "./setup.js";
|
||||
import type { Backend } from "../src/backend/base.js";
|
||||
import { setAgentMode } from "../src/state.js";
|
||||
|
||||
let mockBackend: Backend;
|
||||
|
||||
// Capture console.log and console.error output
|
||||
let output: string;
|
||||
let errOutput: string;
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
|
||||
beforeEach(() => {
|
||||
mockBackend = createMockBackend();
|
||||
output = "";
|
||||
errOutput = "";
|
||||
console.log = (...args: unknown[]) => {
|
||||
output += args.map(String).join(" ") + "\n";
|
||||
};
|
||||
console.error = (...args: unknown[]) => {
|
||||
errOutput += args.map(String).join(" ") + "\n";
|
||||
};
|
||||
});
|
||||
|
||||
// Restore after each test
|
||||
import { afterEach } from "vitest";
|
||||
afterEach(() => {
|
||||
console.log = originalLog;
|
||||
console.error = originalError;
|
||||
setAgentMode(false);
|
||||
});
|
||||
|
||||
describe("cmdAdd", () => {
|
||||
it("adds text memory", async () => {
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "I prefer dark mode", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(mockBackend.add).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("adds from messages JSON", async () => {
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, undefined, {
|
||||
userId: "alice",
|
||||
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(mockBackend.add).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("outputs json format", async () => {
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "json",
|
||||
});
|
||||
expect(output).toContain("results");
|
||||
});
|
||||
|
||||
it("quiet mode produces no memory content", async () => {
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "quiet",
|
||||
});
|
||||
expect(output).not.toContain("dark mode");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdAdd deduplicates PENDING", () => {
|
||||
const DUPLICATE_PENDING = {
|
||||
results: [
|
||||
{ status: "PENDING", event_id: "evt-dup" },
|
||||
{ status: "PENDING", event_id: "evt-dup" },
|
||||
],
|
||||
};
|
||||
|
||||
it("text shows one pending block", async () => {
|
||||
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||
});
|
||||
|
||||
it("json shows one pending entry", async () => {
|
||||
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "json",
|
||||
});
|
||||
const data = JSON.parse(output);
|
||||
const pending = data.results.filter((r: Record<string, unknown>) => r.status === "PENDING");
|
||||
expect(pending).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("agent shows one pending entry", async () => {
|
||||
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||
setAgentMode(true);
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "agent",
|
||||
});
|
||||
const data = JSON.parse(output);
|
||||
expect(data.count).toBe(1);
|
||||
expect(data.data).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdSearch", () => {
|
||||
it("searches and shows results in text mode", async () => {
|
||||
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||
await cmdSearch(mockBackend, "preferences", {
|
||||
userId: "alice",
|
||||
topK: 10,
|
||||
threshold: 0.3,
|
||||
rerank: false,
|
||||
keyword: false,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(output).toContain("Found 2");
|
||||
});
|
||||
|
||||
it("outputs json format", async () => {
|
||||
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||
await cmdSearch(mockBackend, "preferences", {
|
||||
userId: "alice",
|
||||
topK: 10,
|
||||
threshold: 0.3,
|
||||
rerank: false,
|
||||
keyword: false,
|
||||
|
||||
output: "json",
|
||||
});
|
||||
expect(output).toContain("memory");
|
||||
});
|
||||
|
||||
it("shows no results message", async () => {
|
||||
(mockBackend.search as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||
await cmdSearch(mockBackend, "nonexistent", {
|
||||
userId: "alice",
|
||||
topK: 10,
|
||||
threshold: 0.3,
|
||||
rerank: false,
|
||||
keyword: false,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(errOutput).toContain("No memories found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdGet", () => {
|
||||
it("gets memory in text mode", async () => {
|
||||
const { cmdGet } = await import("../src/commands/memory.js");
|
||||
await cmdGet(mockBackend, "abc-123-def-456", { output: "text" });
|
||||
expect(output).toContain("dark mode");
|
||||
});
|
||||
|
||||
it("gets memory in json mode", async () => {
|
||||
const { cmdGet } = await import("../src/commands/memory.js");
|
||||
await cmdGet(mockBackend, "abc-123-def-456", { output: "json" });
|
||||
expect(output).toContain("memory");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdList", () => {
|
||||
it("lists in table mode", async () => {
|
||||
const { cmdList } = await import("../src/commands/memory.js");
|
||||
await cmdList(mockBackend, {
|
||||
userId: "alice",
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
|
||||
output: "table",
|
||||
});
|
||||
expect(output).toContain("dark mode");
|
||||
});
|
||||
|
||||
it("shows empty message", async () => {
|
||||
(mockBackend.listMemories as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||
const { cmdList } = await import("../src/commands/memory.js");
|
||||
await cmdList(mockBackend, {
|
||||
userId: "alice",
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(errOutput).toContain("No memories found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdUpdate", () => {
|
||||
it("updates memory", async () => {
|
||||
const { cmdUpdate } = await import("../src/commands/memory.js");
|
||||
await cmdUpdate(mockBackend, "abc-123", "New text", { output: "text" });
|
||||
expect(output.toLowerCase()).toContain("updated");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdDelete", () => {
|
||||
it("deletes memory", async () => {
|
||||
const { cmdDelete } = await import("../src/commands/memory.js");
|
||||
await cmdDelete(mockBackend, "abc-123", { output: "text" });
|
||||
expect(output.toLowerCase()).toContain("deleted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdDeleteAll", () => {
|
||||
it("deletes all with force", async () => {
|
||||
const { cmdDeleteAll } = await import("../src/commands/memory.js");
|
||||
await cmdDeleteAll(mockBackend, {
|
||||
force: true,
|
||||
userId: "alice",
|
||||
output: "text",
|
||||
});
|
||||
expect(output.toLowerCase()).toContain("deleted");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("cmdEntitiesList", () => {
|
||||
it("lists users in table mode", async () => {
|
||||
const { cmdEntitiesList } = await import("../src/commands/entities.js");
|
||||
await cmdEntitiesList(mockBackend, "users", { output: "table" });
|
||||
expect(output).toContain("alice");
|
||||
});
|
||||
|
||||
it("lists in json mode", async () => {
|
||||
const { cmdEntitiesList } = await import("../src/commands/entities.js");
|
||||
await cmdEntitiesList(mockBackend, "users", { output: "json" });
|
||||
expect(output).toContain("alice");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdEventList", () => {
|
||||
it("lists events in table mode", async () => {
|
||||
const { cmdEventList } = await import("../src/commands/events.js");
|
||||
await cmdEventList(mockBackend, { output: "table" });
|
||||
expect(output).toContain("evt-abc-");
|
||||
expect(output).toContain("ADD");
|
||||
expect(output).toContain("SUCCEEDED");
|
||||
});
|
||||
|
||||
it("lists events in json mode", async () => {
|
||||
const { cmdEventList } = await import("../src/commands/events.js");
|
||||
await cmdEventList(mockBackend, { output: "json" });
|
||||
expect(output).toContain("evt-abc-123-def-456");
|
||||
expect(output).toContain("evt-def-456-ghi-789");
|
||||
});
|
||||
|
||||
it("shows empty message when no events", async () => {
|
||||
(mockBackend.listEvents as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||
const { cmdEventList } = await import("../src/commands/events.js");
|
||||
await cmdEventList(mockBackend, { output: "table" });
|
||||
expect((output + errOutput).toLowerCase()).toContain("no events");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdEventStatus", () => {
|
||||
it("shows event details in text mode", async () => {
|
||||
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "text" });
|
||||
expect(output).toContain("evt-abc-123-def-456");
|
||||
expect(output).toContain("SUCCEEDED");
|
||||
});
|
||||
|
||||
it("shows event details in json mode", async () => {
|
||||
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "json" });
|
||||
expect(output).toContain("evt-abc-123-def-456");
|
||||
expect(output).toContain("ADD");
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent mode", () => {
|
||||
it("cmdAdd outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test preference", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "agent",
|
||||
});
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("add");
|
||||
expect(parsed.data).toBeDefined();
|
||||
expect(parsed.scope).toMatchObject({ user_id: "alice" });
|
||||
expect(Object.keys(parsed.data[0]).sort()).toEqual(["event", "id", "memory"].sort());
|
||||
});
|
||||
|
||||
it("cmdSearch outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||
await cmdSearch(mockBackend, "preferences", {
|
||||
userId: "alice",
|
||||
topK: 10,
|
||||
threshold: 0.3,
|
||||
rerank: false,
|
||||
keyword: false,
|
||||
|
||||
output: "agent",
|
||||
});
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("search");
|
||||
expect(Array.isArray(parsed.data)).toBe(true);
|
||||
expect(parsed.count).toBe(2);
|
||||
const keys = Object.keys(parsed.data[0]);
|
||||
expect(keys).toContain("id");
|
||||
expect(keys).toContain("memory");
|
||||
expect(keys).toContain("score");
|
||||
expect(keys).toContain("created_at");
|
||||
expect(keys).toContain("categories");
|
||||
expect(keys).not.toContain("user_id");
|
||||
expect(keys).not.toContain("agent_id");
|
||||
});
|
||||
|
||||
it("cmdList outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdList } = await import("../src/commands/memory.js");
|
||||
await cmdList(mockBackend, {
|
||||
userId: "alice",
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
|
||||
output: "agent",
|
||||
});
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("list");
|
||||
expect(Array.isArray(parsed.data)).toBe(true);
|
||||
expect(parsed.count).toBe(2);
|
||||
expect(Object.keys(parsed.data[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||
});
|
||||
|
||||
it("cmdGet outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdGet } = await import("../src/commands/memory.js");
|
||||
await cmdGet(mockBackend, "abc-123-def-456", { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("get");
|
||||
expect(parsed.data).toBeDefined();
|
||||
expect(parsed.data).toMatchObject({ id: "abc-123-def-456" });
|
||||
expect(Object.keys(parsed.data)).not.toContain("user_id");
|
||||
});
|
||||
|
||||
it("cmdUpdate outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdUpdate } = await import("../src/commands/memory.js");
|
||||
await cmdUpdate(mockBackend, "abc-123", "Updated text", { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("update");
|
||||
expect(parsed.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("cmdDelete outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdDelete } = await import("../src/commands/memory.js");
|
||||
await cmdDelete(mockBackend, "abc-123", { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("delete");
|
||||
expect(parsed.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("cmdEventList outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdEventList } = await import("../src/commands/events.js");
|
||||
await cmdEventList(mockBackend, { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("event list");
|
||||
expect(Array.isArray(parsed.data)).toBe(true);
|
||||
expect(parsed.count).toBe(2);
|
||||
expect(Object.keys(parsed.data[0]).sort()).toEqual(
|
||||
["created_at", "event_type", "id", "latency", "status"],
|
||||
);
|
||||
expect(Object.keys(parsed.data[0])).not.toContain("updated_at");
|
||||
});
|
||||
|
||||
it("cmdEventStatus outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("event status");
|
||||
expect(parsed.data).toBeDefined();
|
||||
expect(parsed.data).toMatchObject({ id: "evt-abc-123-def-456" });
|
||||
expect(parsed.data.results[0]).toHaveProperty("memory");
|
||||
expect(parsed.data.results[0]).not.toHaveProperty("data");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Tests for configuration management.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
createDefaultConfig,
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
redactKey,
|
||||
getNestedValue,
|
||||
setNestedValue,
|
||||
CONFIG_DIR,
|
||||
CONFIG_FILE,
|
||||
} from "../src/config.js";
|
||||
|
||||
// Use a temp directory for config during tests
|
||||
let origConfigDir: string;
|
||||
let origConfigFile: string;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
// Monkey-patch the module-level constants
|
||||
// We'll use env vars and direct file manipulation instead
|
||||
// Clear MEM0_ env vars
|
||||
for (const key of Object.keys(process.env)) {
|
||||
if (key.startsWith("MEM0_")) {
|
||||
delete process.env[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("redactKey", () => {
|
||||
it("returns '(not set)' for empty key", () => {
|
||||
expect(redactKey("")).toBe("(not set)");
|
||||
});
|
||||
|
||||
it("redacts short key", () => {
|
||||
expect(redactKey("abc")).toBe("ab***");
|
||||
});
|
||||
|
||||
it("redacts normal key", () => {
|
||||
const result = redactKey("m0-abcdefgh12345678");
|
||||
expect(result).toBe("m0-a...5678");
|
||||
expect(result).not.toContain("abcdefgh");
|
||||
});
|
||||
|
||||
it("redacts exactly 8-char key as short", () => {
|
||||
expect(redactKey("12345678")).toBe("12***");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDefaultConfig", () => {
|
||||
it("has correct defaults", () => {
|
||||
const config = createDefaultConfig();
|
||||
expect(config.platform.baseUrl).toBe("https://api.mem0.ai");
|
||||
expect(config.platform.apiKey).toBe("");
|
||||
expect(config.defaults.userId).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNestedValue", () => {
|
||||
it("gets platform.api_key", () => {
|
||||
const config = createDefaultConfig();
|
||||
config.platform.apiKey = "test-key";
|
||||
expect(getNestedValue(config, "platform.api_key")).toBe("test-key");
|
||||
});
|
||||
|
||||
it("returns undefined for nonexistent key", () => {
|
||||
const config = createDefaultConfig();
|
||||
expect(getNestedValue(config, "nonexistent.key")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("gets defaults.user_id", () => {
|
||||
const config = createDefaultConfig();
|
||||
config.defaults.userId = "alice";
|
||||
expect(getNestedValue(config, "defaults.user_id")).toBe("alice");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setNestedValue", () => {
|
||||
it("sets platform.api_key", () => {
|
||||
const config = createDefaultConfig();
|
||||
expect(setNestedValue(config, "platform.api_key", "new-key")).toBe(true);
|
||||
expect(config.platform.apiKey).toBe("new-key");
|
||||
});
|
||||
|
||||
it("returns false for nonexistent key", () => {
|
||||
const config = createDefaultConfig();
|
||||
expect(setNestedValue(config, "nonexistent.key", "val")).toBe(false);
|
||||
});
|
||||
|
||||
it("sets defaults.user_id", () => {
|
||||
const config = createDefaultConfig();
|
||||
expect(setNestedValue(config, "defaults.user_id", "bob")).toBe(true);
|
||||
expect(config.defaults.userId).toBe("bob");
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Unit tests for init internals — decision tree primitives + plugin sync.
|
||||
*
|
||||
* Mirror of `cli/python/tests/test_init_internals.py`. Both files MUST stay
|
||||
* in sync — if you add a behavioral assertion here, mirror it on the Python
|
||||
* side and vice versa.
|
||||
*
|
||||
* - `pingKey` must NOT treat network errors as "invalid key" (else a VPN
|
||||
* flap silently mints a new shadow over a working key).
|
||||
* - `plugin_sync` must only update entries that already exist, preserve
|
||||
* trailing newlines, and never mangle other lines.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { pingKey } from "../src/commands/init.js";
|
||||
import { updateClaudeSettings, updateShellRc } from "../src/plugin-sync.js";
|
||||
|
||||
// ── pingKey ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe("pingKey — network vs auth distinction", () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
afterEach(() => {
|
||||
globalThis.fetch = origFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns true for 200", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for 401 (definitively invalid)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 401 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for 403 (definitively invalid)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 403 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for 5xx (transient upstream — prefer reuse)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 503 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true on network error (prefer reuse over re-mint)", async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true on timeout (prefer reuse)", async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("aborted"));
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── updateShellRc ────────────────────────────────────────────────────────
|
||||
|
||||
describe("updateShellRc — exists-only contract", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("updates existing export and preserves trailing newline", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, 'export MEM0_API_KEY="old"\n');
|
||||
expect(updateShellRc(rc, "newkey")).toBe(true);
|
||||
expect(fs.readFileSync(rc, "utf-8")).toBe('export MEM0_API_KEY="newkey"\n');
|
||||
});
|
||||
|
||||
it("does NOT create a new export when none exists", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, "alias ll='ls -la'\n");
|
||||
expect(updateShellRc(rc, "newkey")).toBe(false);
|
||||
expect(fs.readFileSync(rc, "utf-8")).toBe("alias ll='ls -la'\n");
|
||||
});
|
||||
|
||||
it("preserves surrounding content", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
const original =
|
||||
"# my zshrc\n" +
|
||||
"alias ll='ls -la'\n" +
|
||||
"export MEM0_API_KEY='old'\n" +
|
||||
"export OTHER=keepme\n";
|
||||
fs.writeFileSync(rc, original);
|
||||
updateShellRc(rc, "newkey");
|
||||
const after = fs.readFileSync(rc, "utf-8");
|
||||
expect(after).toContain("alias ll='ls -la'\n");
|
||||
expect(after).toContain("export OTHER=keepme\n");
|
||||
expect(after).toContain("# my zshrc\n");
|
||||
expect(after).toContain('export MEM0_API_KEY="newkey"\n');
|
||||
});
|
||||
|
||||
it("is idempotent when value already matches", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, 'export MEM0_API_KEY="same"\n');
|
||||
expect(updateShellRc(rc, "same")).toBe(false);
|
||||
});
|
||||
|
||||
it("is a no-op for missing files", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc"); // does not exist
|
||||
expect(updateShellRc(rc, "x")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── updateClaudeSettings ─────────────────────────────────────────────────
|
||||
|
||||
describe("updateClaudeSettings — never creates entries", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("does not create env block when none exists", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, JSON.stringify({ otherKey: 1 }));
|
||||
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
|
||||
expect(JSON.parse(fs.readFileSync(settings, "utf-8"))).toEqual({
|
||||
otherKey: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not create MEM0_API_KEY entry in existing env block", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, JSON.stringify({ env: { OTHER_KEY: "x" } }));
|
||||
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
|
||||
});
|
||||
|
||||
it("updates existing entry and preserves siblings", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(
|
||||
settings,
|
||||
JSON.stringify({ env: { MEM0_API_KEY: "old", OTHER: "y" } }, null, 2),
|
||||
);
|
||||
expect(updateClaudeSettings(settings, "fresh")).toBe(true);
|
||||
const data = JSON.parse(fs.readFileSync(settings, "utf-8"));
|
||||
expect(data.env.MEM0_API_KEY).toBe("fresh");
|
||||
expect(data.env.OTHER).toBe("y");
|
||||
});
|
||||
|
||||
it("is idempotent when value already matches", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(
|
||||
settings,
|
||||
JSON.stringify({ env: { MEM0_API_KEY: "same" } }),
|
||||
);
|
||||
expect(updateClaudeSettings(settings, "same")).toBe(false);
|
||||
});
|
||||
|
||||
it("is a no-op for malformed JSON", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, "{ this is not json");
|
||||
expect(updateClaudeSettings(settings, "x")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Tests for output formatting.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
formatMemoriesText,
|
||||
formatMemoriesTable,
|
||||
formatJson,
|
||||
formatSingleMemory,
|
||||
formatAddResult,
|
||||
printResultSummary,
|
||||
sanitizeAgentData,
|
||||
} from "../src/output.js";
|
||||
|
||||
let output: string;
|
||||
const originalLog = console.log;
|
||||
|
||||
beforeEach(() => {
|
||||
output = "";
|
||||
console.log = (...args: unknown[]) => {
|
||||
output += args.map(String).join(" ") + "\n";
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.log = originalLog;
|
||||
});
|
||||
|
||||
const sampleMemories = [
|
||||
{
|
||||
id: "abc-123-def-456",
|
||||
memory: "User prefers dark mode",
|
||||
score: 0.92,
|
||||
created_at: "2026-02-15T10:30:00Z",
|
||||
categories: ["preferences"],
|
||||
},
|
||||
{
|
||||
id: "ghi-789-jkl-012",
|
||||
memory: "User uses vim keybindings",
|
||||
score: 0.78,
|
||||
created_at: "2026-03-01T14:00:00Z",
|
||||
categories: ["tools"],
|
||||
},
|
||||
];
|
||||
|
||||
describe("formatMemoriesText", () => {
|
||||
it("shows count and memory content", () => {
|
||||
formatMemoriesText(sampleMemories);
|
||||
expect(output).toContain("Found 2");
|
||||
expect(output).toContain("dark mode");
|
||||
expect(output).toContain("vim keybindings");
|
||||
});
|
||||
|
||||
it("shows scores and IDs", () => {
|
||||
formatMemoriesText(sampleMemories);
|
||||
expect(output).toContain("0.92");
|
||||
expect(output).toContain("abc-123-");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatMemoriesTable", () => {
|
||||
it("renders a table with memory content", () => {
|
||||
formatMemoriesTable(sampleMemories);
|
||||
expect(output).toContain("dark mode");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatJson", () => {
|
||||
it("outputs valid JSON", () => {
|
||||
formatJson({ key: "value" });
|
||||
expect(JSON.parse(output)).toEqual({ key: "value" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSingleMemory", () => {
|
||||
it("shows memory text in text mode", () => {
|
||||
formatSingleMemory(sampleMemories[0], "text");
|
||||
expect(output).toContain("dark mode");
|
||||
});
|
||||
|
||||
it("outputs JSON in json mode", () => {
|
||||
formatSingleMemory(sampleMemories[0], "json");
|
||||
expect(output).toContain("memory");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatAddResult", () => {
|
||||
it("shows ADD event", () => {
|
||||
formatAddResult({
|
||||
results: [{ id: "abc-123", memory: "Test", event: "ADD" }],
|
||||
});
|
||||
expect(output).toContain("Added");
|
||||
});
|
||||
|
||||
it("shows PENDING event", () => {
|
||||
formatAddResult({
|
||||
results: [{ status: "PENDING", event_id: "evt-12345678" }],
|
||||
});
|
||||
expect(output).toContain("Queued");
|
||||
});
|
||||
|
||||
it("deduplicates PENDING entries with same event_id", () => {
|
||||
formatAddResult({
|
||||
results: [
|
||||
{ status: "PENDING", event_id: "evt-dup" },
|
||||
{ status: "PENDING", event_id: "evt-dup" },
|
||||
],
|
||||
});
|
||||
// Should show only one PENDING block despite two entries with same event_id
|
||||
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||
expect(output.match(/evt-dup/g)?.length).toBe(2); // event_id line + status hint line
|
||||
});
|
||||
});
|
||||
|
||||
describe("printResultSummary", () => {
|
||||
it("shows count and duration", () => {
|
||||
printResultSummary({ count: 5, durationSecs: 1.23 });
|
||||
expect(output).toContain("5 results");
|
||||
expect(output).toContain("1.23s");
|
||||
});
|
||||
|
||||
it("handles singular", () => {
|
||||
printResultSummary({ count: 1 });
|
||||
expect(output).toContain("1 result");
|
||||
expect(output).not.toContain("results");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeAgentData", () => {
|
||||
it("projects add results", () => {
|
||||
const raw = [{ id: "abc", memory: "test", event: "ADD", metadata: { x: 1 }, categories: ["a"] }];
|
||||
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||
expect(result).toEqual([{ id: "abc", memory: "test", event: "ADD" }]);
|
||||
});
|
||||
|
||||
it("passes through PENDING add items", () => {
|
||||
const raw = [{ status: "PENDING", event_id: "evt-123", noise: "x" }];
|
||||
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||
expect(result).toEqual([{ status: "PENDING", event_id: "evt-123" }]);
|
||||
});
|
||||
|
||||
it("projects search results", () => {
|
||||
const raw = [{ id: "abc", memory: "test", score: 0.9, created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||
const result = sanitizeAgentData("search", raw) as Record<string, unknown>[];
|
||||
expect(result[0]).not.toHaveProperty("user_id");
|
||||
expect(result[0]).toHaveProperty("score");
|
||||
});
|
||||
|
||||
it("projects list results", () => {
|
||||
const raw = [{ id: "abc", memory: "test", created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||
const result = sanitizeAgentData("list", raw) as Record<string, unknown>[];
|
||||
expect(Object.keys(result[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||
});
|
||||
|
||||
it("projects get result", () => {
|
||||
const raw = { id: "abc", memory: "test", created_at: "2026-01-01", updated_at: "2026-01-02", categories: ["a"], metadata: { k: "v" }, user_id: "u1" };
|
||||
const result = sanitizeAgentData("get", raw) as Record<string, unknown>;
|
||||
expect(result).not.toHaveProperty("user_id");
|
||||
expect(result).toHaveProperty("metadata");
|
||||
});
|
||||
|
||||
it("projects update result", () => {
|
||||
const raw = { id: "abc", memory: "updated", extra: "noise" };
|
||||
const result = sanitizeAgentData("update", raw);
|
||||
expect(result).toEqual({ id: "abc", memory: "updated" });
|
||||
});
|
||||
|
||||
it("projects event list results", () => {
|
||||
const raw = [{ id: "evt-1", event_type: "ADD", status: "SUCCEEDED", graph_status: null, latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02" }];
|
||||
const result = sanitizeAgentData("event list", raw) as Record<string, unknown>[];
|
||||
expect(result[0]).not.toHaveProperty("updated_at");
|
||||
expect(result[0]).not.toHaveProperty("graph_status");
|
||||
});
|
||||
|
||||
it("flattens event status results", () => {
|
||||
const raw = {
|
||||
id: "evt-1", event_type: "ADD", status: "SUCCEEDED",
|
||||
latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02",
|
||||
results: [{ id: "mem-1", event: "ADD", user_id: "alice", data: { memory: "dark mode" } }],
|
||||
};
|
||||
const result = sanitizeAgentData("event status", raw) as Record<string, unknown>;
|
||||
const firstResult = (result.results as Record<string, unknown>[])[0];
|
||||
expect(firstResult).toHaveProperty("memory", "dark mode");
|
||||
expect(firstResult).not.toHaveProperty("data");
|
||||
});
|
||||
|
||||
it("passes through status/config/import commands unchanged", () => {
|
||||
const data = { key: "value", other: "stuff" };
|
||||
for (const cmd of ["status", "import", "config show", "config get", "config set"]) {
|
||||
expect(sanitizeAgentData(cmd, data)).toEqual(data);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles null data", () => {
|
||||
expect(sanitizeAgentData("add", null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Shared test helpers and mock factories for mem0 CLI tests.
|
||||
*/
|
||||
|
||||
import { vi } from "vitest";
|
||||
import type { Backend } from "../src/backend/base.js";
|
||||
|
||||
/** Create a mock backend with all methods stubbed with sensible defaults. */
|
||||
export function createMockBackend(): Backend {
|
||||
return {
|
||||
add: vi.fn().mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
id: "abc-123-def-456",
|
||||
memory: "User prefers dark mode",
|
||||
event: "ADD",
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
search: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "abc-123-def-456",
|
||||
memory: "User prefers dark mode",
|
||||
score: 0.92,
|
||||
created_at: "2026-02-15T10:30:00Z",
|
||||
categories: ["preferences"],
|
||||
},
|
||||
{
|
||||
id: "ghi-789-jkl-012",
|
||||
memory: "User uses vim keybindings",
|
||||
score: 0.78,
|
||||
created_at: "2026-03-01T14:00:00Z",
|
||||
categories: ["tools"],
|
||||
},
|
||||
]),
|
||||
|
||||
get: vi.fn().mockResolvedValue({
|
||||
id: "abc-123-def-456",
|
||||
memory: "User prefers dark mode",
|
||||
created_at: "2026-02-15T10:30:00Z",
|
||||
updated_at: "2026-02-20T08:00:00Z",
|
||||
metadata: { source: "onboarding" },
|
||||
categories: ["preferences"],
|
||||
}),
|
||||
|
||||
listMemories: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "abc-123-def-456",
|
||||
memory: "User prefers dark mode",
|
||||
created_at: "2026-02-15T10:30:00Z",
|
||||
categories: ["preferences"],
|
||||
},
|
||||
{
|
||||
id: "ghi-789-jkl-012",
|
||||
memory: "User uses vim keybindings",
|
||||
created_at: "2026-03-01T14:00:00Z",
|
||||
categories: ["tools"],
|
||||
},
|
||||
]),
|
||||
|
||||
update: vi.fn().mockResolvedValue({ id: "abc-123-def-456", memory: "Updated memory" }),
|
||||
delete: vi.fn().mockResolvedValue({ status: "deleted" }),
|
||||
status: vi.fn().mockResolvedValue({
|
||||
connected: true,
|
||||
backend: "platform",
|
||||
base_url: "https://api.mem0.ai",
|
||||
}),
|
||||
deleteEntities: vi.fn().mockResolvedValue({ message: "Entity deleted" }),
|
||||
entities: vi.fn().mockResolvedValue([
|
||||
{ name: "alice", count: 5 },
|
||||
{ name: "bob", count: 3 },
|
||||
]),
|
||||
listEvents: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "evt-abc-123-def-456",
|
||||
event_type: "ADD",
|
||||
status: "SUCCEEDED",
|
||||
graph_status: null,
|
||||
latency: 1234.5,
|
||||
created_at: "2026-04-01T10:00:00Z",
|
||||
updated_at: "2026-04-01T10:00:01Z",
|
||||
},
|
||||
{
|
||||
id: "evt-def-456-ghi-789",
|
||||
event_type: "SEARCH",
|
||||
status: "PENDING",
|
||||
graph_status: null,
|
||||
latency: null,
|
||||
created_at: "2026-04-01T10:01:00Z",
|
||||
updated_at: "2026-04-01T10:01:00Z",
|
||||
},
|
||||
]),
|
||||
getEvent: vi.fn().mockResolvedValue({
|
||||
id: "evt-abc-123-def-456",
|
||||
event_type: "ADD",
|
||||
status: "SUCCEEDED",
|
||||
graph_status: "SUCCEEDED",
|
||||
latency: 1234.5,
|
||||
created_at: "2026-04-01T10:00:00Z",
|
||||
updated_at: "2026-04-01T10:00:01Z",
|
||||
results: [
|
||||
{
|
||||
id: "mem-abc-123",
|
||||
event: "ADD",
|
||||
user_id: "alice",
|
||||
data: { memory: "User prefers dark mode" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
const pkg = _require('./package.json');
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
define: {
|
||||
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createRequire } from "node:module";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
const pkg = _require("./package.json") as { version: string };
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
VENV := .venv
|
||||
PYTHON := $(VENV)/bin/python
|
||||
PIP := $(VENV)/bin/pip
|
||||
|
||||
.PHONY: install dev lint format test build clean publish publish-test shell
|
||||
|
||||
$(VENV)/bin/activate:
|
||||
python3 -m venv $(VENV)
|
||||
$(PIP) install -U pip
|
||||
|
||||
install: $(VENV)/bin/activate
|
||||
$(PIP) install -e .
|
||||
|
||||
dev: $(VENV)/bin/activate
|
||||
$(PIP) install -e ".[dev]"
|
||||
|
||||
lint: dev
|
||||
$(VENV)/bin/ruff check .
|
||||
$(VENV)/bin/ruff format --check .
|
||||
|
||||
format: dev
|
||||
$(VENV)/bin/ruff check --fix .
|
||||
$(VENV)/bin/ruff format .
|
||||
|
||||
test: dev
|
||||
$(VENV)/bin/pytest
|
||||
|
||||
build: clean $(VENV)/bin/activate
|
||||
$(PIP) install hatch
|
||||
$(VENV)/bin/hatch build
|
||||
|
||||
clean:
|
||||
rm -rf dist/
|
||||
|
||||
publish: build
|
||||
$(VENV)/bin/hatch publish
|
||||
|
||||
publish-test: build
|
||||
$(VENV)/bin/hatch publish --repo test
|
||||
|
||||
shell: $(VENV)/bin/activate
|
||||
@echo "Spawning a new shell with the virtual environment activated..."
|
||||
@VIRTUAL_ENV=$(CURDIR)/$(VENV) PATH=$(CURDIR)/$(VENV)/bin:$$PATH exec $(SHELL)
|
||||
@@ -0,0 +1,349 @@
|
||||
# mem0 CLI (Python)
|
||||
|
||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Python implementation.
|
||||
|
||||
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python **3.10+**
|
||||
|
||||
## Installation
|
||||
|
||||
### Using pipx (recommended)
|
||||
|
||||
```bash
|
||||
pipx install mem0-cli
|
||||
```
|
||||
|
||||
### Using pip
|
||||
|
||||
```bash
|
||||
pip install mem0-cli
|
||||
```
|
||||
|
||||
> **Note:** On macOS with Homebrew Python, `pip install` outside a virtual environment will fail with an `externally-managed-environment` error ([PEP 668](https://peps.python.org/pep-0668/)). Use `pipx` instead, or install inside a virtual environment.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Interactive setup wizard
|
||||
mem0 init
|
||||
|
||||
# Or login via email
|
||||
mem0 init --email alice@company.com
|
||||
|
||||
# Or authenticate with an existing API key
|
||||
mem0 init --api-key m0-xxx
|
||||
|
||||
# Add a memory
|
||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||
|
||||
# Search memories
|
||||
mem0 search "What are Alice's preferences?" --user-id alice
|
||||
|
||||
# List all memories for a user
|
||||
mem0 list --user-id alice
|
||||
|
||||
# Get a specific memory
|
||||
mem0 get <memory-id>
|
||||
|
||||
# Update a memory
|
||||
mem0 update <memory-id> "I switched to light mode"
|
||||
|
||||
# Delete a memory
|
||||
mem0 delete <memory-id>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `mem0 init`
|
||||
|
||||
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||
|
||||
```bash
|
||||
mem0 init
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
mem0 init --email alice@company.com
|
||||
```
|
||||
|
||||
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||
|
||||
```bash
|
||||
mem0 init --api-key m0-xxx --user-id alice --force
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--api-key` | API key (skip prompt) |
|
||||
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||
| `--email` | Login via email verification code |
|
||||
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||
| `--force` | Overwrite existing config without confirmation |
|
||||
|
||||
### `mem0 add`
|
||||
|
||||
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||
|
||||
```bash
|
||||
mem0 add "I prefer dark mode" --user-id alice
|
||||
mem0 add --file conversation.json --user-id alice
|
||||
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Scope to a user |
|
||||
| `--agent-id` | Scope to an agent |
|
||||
| `--messages` | Conversation messages as JSON |
|
||||
| `-f, --file` | Read messages from a JSON file |
|
||||
| `-m, --metadata` | Custom metadata as JSON |
|
||||
| `--categories` | Categories (JSON array or comma-separated) |
|
||||
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||
|
||||
### `mem0 search`
|
||||
|
||||
Search memories using natural language.
|
||||
|
||||
```bash
|
||||
mem0 search "dietary restrictions" --user-id alice
|
||||
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `-k, --top-k` | Number of results (default: 10) |
|
||||
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||
| `--rerank` | Enable reranking |
|
||||
| `--keyword` | Use keyword search instead of semantic |
|
||||
| `--filter` | Advanced filter expression (JSON) |
|
||||
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 list`
|
||||
|
||||
List memories with optional filters and pagination.
|
||||
|
||||
```bash
|
||||
mem0 list --user-id alice
|
||||
mem0 list --user-id alice --category preferences --output json
|
||||
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `--page` | Page number (default: 1) |
|
||||
| `--page-size` | Results per page (default: 100) |
|
||||
| `--category` | Filter by category |
|
||||
| `--after` | Created after date (YYYY-MM-DD) |
|
||||
| `--before` | Created before date (YYYY-MM-DD) |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 get`
|
||||
|
||||
Retrieve a specific memory by ID.
|
||||
|
||||
```bash
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||
```
|
||||
|
||||
### `mem0 update`
|
||||
|
||||
Update the text or metadata of an existing memory.
|
||||
|
||||
```bash
|
||||
mem0 update <memory-id> "Updated preference text"
|
||||
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||
echo "new text" | mem0 update <memory-id>
|
||||
```
|
||||
|
||||
### `mem0 delete`
|
||||
|
||||
Delete a single memory, all memories for a scope, or an entire entity.
|
||||
|
||||
```bash
|
||||
# Delete a single memory
|
||||
mem0 delete <memory-id>
|
||||
|
||||
# Delete all memories for a user
|
||||
mem0 delete --all --user-id alice --force
|
||||
|
||||
# Delete all memories project-wide
|
||||
mem0 delete --all --project --force
|
||||
|
||||
# Preview what would be deleted
|
||||
mem0 delete --all --user-id alice --dry-run
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--all` | Delete all memories matching scope filters |
|
||||
| `--entity` | Delete the entity and all its memories |
|
||||
| `--project` | With `--all`: delete all memories project-wide |
|
||||
| `--dry-run` | Preview without deleting |
|
||||
| `--force` | Skip confirmation prompt |
|
||||
|
||||
### `mem0 import`
|
||||
|
||||
Bulk import memories from a JSON file.
|
||||
|
||||
```bash
|
||||
mem0 import data.json --user-id alice
|
||||
```
|
||||
|
||||
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||
|
||||
### `mem0 config`
|
||||
|
||||
View or modify the local CLI configuration.
|
||||
|
||||
```bash
|
||||
mem0 config show # Display current config (secrets redacted)
|
||||
mem0 config get api_key # Get a specific value
|
||||
mem0 config set user_id bob # Set a value
|
||||
```
|
||||
|
||||
### `mem0 entity`
|
||||
|
||||
List or delete entities (users, agents, apps, runs).
|
||||
|
||||
```bash
|
||||
mem0 entity list users
|
||||
mem0 entity list agents --output json
|
||||
mem0 entity delete --user-id alice --force
|
||||
```
|
||||
|
||||
### `mem0 event`
|
||||
|
||||
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||
|
||||
```bash
|
||||
# List recent events
|
||||
mem0 event list
|
||||
|
||||
# Check the status of a specific event
|
||||
mem0 event status <event-id>
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-o, --output` | Output format: `text`, `json` |
|
||||
|
||||
### `mem0 status`
|
||||
|
||||
Verify your API connection and display the current project.
|
||||
|
||||
```bash
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
|
||||
```bash
|
||||
mem0 --agent search "user preferences" --user-id alice
|
||||
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||
mem0 --agent list --user-id alice
|
||||
mem0 --agent delete --all --user-id alice --force
|
||||
```
|
||||
|
||||
Every command returns the same envelope shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "search",
|
||||
"duration_ms": 134,
|
||||
"scope": { "user_id": "alice" },
|
||||
"count": 2,
|
||||
"data": [
|
||||
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
What agent mode does differently from `--output json`:
|
||||
|
||||
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||
|
||||
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||
|
||||
## Output formats
|
||||
|
||||
Control how results are displayed with `--output`:
|
||||
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `text` | Human-readable with colors and formatting (default) |
|
||||
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||
| `table` | Tabular format (default for `list`) |
|
||||
| `quiet` | Minimal — just IDs or status codes |
|
||||
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||
|
||||
## Global flags
|
||||
|
||||
These flags are available on all commands:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||
| `--agent` | Alias for `--json` |
|
||||
| `--api-key` | Override the configured API key for this request |
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||
| `MEM0_BASE_URL` | API base URL |
|
||||
| `MEM0_USER_ID` | Default user ID |
|
||||
| `MEM0_AGENT_ID` | Default agent ID |
|
||||
| `MEM0_APP_ID` | Default app ID |
|
||||
| `MEM0_RUN_ID` | Default run ID |
|
||||
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||
|
||||
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd cli/python
|
||||
python -m venv .venv && source .venv/bin/activate
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Run during development
|
||||
python -m mem0_cli --help
|
||||
mem0 add "test memory" --user-id alice
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
1. Update `version` in `pyproject.toml`
|
||||
2. Create a GitHub Release with tag `cli-v<version>` (e.g. `cli-v0.2.1`)
|
||||
|
||||
For a pre-release, use a beta version like `0.2.1b1` and check the **pre-release** checkbox.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,107 @@
|
||||
# Development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python **3.10+**
|
||||
- `make` (optional — you can use plain Python commands instead)
|
||||
|
||||
All commands below should be run from the `python/` directory:
|
||||
|
||||
```bash
|
||||
cd python
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### Using Make (recommended)
|
||||
|
||||
All `make` targets automatically create a virtual environment (`.venv/`) and install the required dependencies — no manual setup needed.
|
||||
|
||||
```bash
|
||||
# Install the CLI in editable mode
|
||||
make install
|
||||
|
||||
# Install with dev tools (tests + linting)
|
||||
make dev
|
||||
```
|
||||
|
||||
### Using Python directly
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install -U pip
|
||||
|
||||
# Install in editable mode
|
||||
pip install -e .
|
||||
|
||||
# With dev tools
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Make targets
|
||||
|
||||
| Target | Description |
|
||||
| ------------------- | ------------------------------------------------ |
|
||||
| `make install` | Create venv and install the CLI (editable mode) |
|
||||
| `make dev` | Create venv and install CLI + dev dependencies |
|
||||
| `make test` | Run all tests (installs dev deps if needed) |
|
||||
| `make lint` | Run linter and format check |
|
||||
| `make format` | Auto-fix lint issues and format code |
|
||||
| `make build` | Build distribution packages |
|
||||
| `make clean` | Remove `dist/` |
|
||||
| `make publish` | Build and publish to PyPI |
|
||||
| `make publish-test` | Build and publish to Test PyPI |
|
||||
| `make shell` | Open a new shell with the venv activated |
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# Using Make
|
||||
make test
|
||||
|
||||
# Using Python directly
|
||||
pytest
|
||||
|
||||
# Run a specific test file
|
||||
pytest tests/test_cli_integration.py
|
||||
|
||||
# Run a single test
|
||||
pytest -k test_help
|
||||
```
|
||||
|
||||
## Run the CLI
|
||||
|
||||
```bash
|
||||
# Using Make — drop into an activated shell
|
||||
make shell
|
||||
mem0 --help
|
||||
|
||||
# Using Python directly (with venv activated)
|
||||
source .venv/bin/activate
|
||||
mem0 --help
|
||||
mem0 version
|
||||
|
||||
# Or run without activating
|
||||
.venv/bin/mem0 --help
|
||||
```
|
||||
|
||||
## Lint
|
||||
|
||||
```bash
|
||||
# Using Make
|
||||
make lint # check only
|
||||
make format # auto-fix
|
||||
|
||||
# Using Python directly (with venv activated)
|
||||
ruff check .
|
||||
ruff format .
|
||||
```
|
||||
|
||||
## Optional extras
|
||||
|
||||
### OSS integration
|
||||
|
||||
```bash
|
||||
pip install -e ".[oss]"
|
||||
```
|
||||
@@ -0,0 +1,77 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0-cli"
|
||||
version = "0.2.5"
|
||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "mem0.ai", email = "founders@mem0.ai" },
|
||||
]
|
||||
keywords = ["mem0", "memory", "ai", "agents", "cli"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
]
|
||||
dependencies = [
|
||||
"typer>=0.9.0",
|
||||
"rich>=13.0.0",
|
||||
"httpx>=0.24.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
oss = ["mem0ai>=0.1.0"]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
"pytest-asyncio>=0.21",
|
||||
"ruff>=0.1.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mem0 = "mem0_cli.app:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/mem0_cli"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["src/mem0_cli"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"F", # pyflakes
|
||||
"I", # isort (import sorting)
|
||||
"W", # pycodestyle warnings
|
||||
"UP", # pyupgrade (modern Python syntax)
|
||||
"B", # flake8-bugbear (common bugs)
|
||||
"SIM", # flake8-simplify
|
||||
"RUF", # ruff-specific rules
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long — handled by formatter
|
||||
"B008", # function call in default arg — required by Typer's Option/Argument pattern
|
||||
"SIM108", # ternary operator — sometimes less readable
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["mem0_cli"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
docstring-code-format = true
|
||||
@@ -0,0 +1,3 @@
|
||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||
|
||||
__version__ = "0.2.4"
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Allow running with `python -m mem0_cli`."""
|
||||
|
||||
from mem0_cli.app import main
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Detect whether the CLI is being invoked from inside an AI-agent context.
|
||||
|
||||
Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
|
||||
agent runtime env var is present. The return value is a context **trigger
|
||||
only** — the canonical agent identity is self-declared by the agent via
|
||||
``--agent-caller <name>`` (Proof Editor-style) and never sniffed from env
|
||||
vars to fill the ``agent_caller`` field on the APIKey row.
|
||||
|
||||
Returns a short name or None. The list is curated, not exhaustive — env
|
||||
vars we don't recognise fall through to None (caller treated as
|
||||
non-agent). Honest reporting depends on ``--agent-caller``; this list is
|
||||
just enough to enable the zero-friction auto-bootstrap UX.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
_AGENT_CALLER_ENV: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("claude-code", ("CLAUDECODE", "CLAUDE_CODE")),
|
||||
("cursor", ("CURSOR_AGENT", "CURSOR_SESSION_ID")),
|
||||
("codex", ("CODEX_CLI", "OPENAI_CODEX")),
|
||||
("cline", ("CLINE_AGENT", "CLINE")),
|
||||
("continue", ("CONTINUE_AGENT", "CONTINUE_SESSION")),
|
||||
("aider", ("AIDER_SESSION",)),
|
||||
("goose", ("GOOSE_AGENT",)),
|
||||
("windsurf", ("WINDSURF_AGENT",)),
|
||||
)
|
||||
|
||||
|
||||
def detect_agent_caller() -> str | None:
|
||||
"""Return a canonical agent name if any agent env var is set, else None."""
|
||||
for name, env_vars in _AGENT_CALLER_ENV:
|
||||
if any(os.environ.get(v) for v in env_vars):
|
||||
return name
|
||||
return None
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
"""Backend abstraction layer for mem0 CLI."""
|
||||
|
||||
from mem0_cli.backend.base import Backend, get_backend
|
||||
|
||||
__all__ = ["Backend", "get_backend"]
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Abstract backend interface and factory."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from mem0_cli.config import Mem0Config
|
||||
|
||||
|
||||
class Backend(ABC):
|
||||
"""Abstract interface for mem0 backends."""
|
||||
|
||||
@abstractmethod
|
||||
def add(
|
||||
self,
|
||||
content: str | None = None,
|
||||
messages: list[dict] | None = None,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
immutable: bool = False,
|
||||
infer: bool = True,
|
||||
expires: str | None = None,
|
||||
categories: list[str] | None = None,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
top_k: int = 10,
|
||||
threshold: float = 0.3,
|
||||
rerank: bool = False,
|
||||
keyword: bool = False,
|
||||
filters: dict | None = None,
|
||||
fields: list[str] | None = None,
|
||||
) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
def get(self, memory_id: str) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
def list_memories(
|
||||
self,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 100,
|
||||
category: str | None = None,
|
||||
after: str | None = None,
|
||||
before: str | None = None,
|
||||
) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
def update(
|
||||
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
def delete(
|
||||
self,
|
||||
memory_id: str | None = None,
|
||||
*,
|
||||
all: bool = False,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
def delete_entities(
|
||||
self,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
def status(
|
||||
self,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
@abstractmethod
|
||||
def entities(self, entity_type: str) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
def list_events(self) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
def get_event(self, event_id: str) -> dict: ...
|
||||
|
||||
|
||||
def get_backend(config: Mem0Config) -> Backend:
|
||||
"""Return the Platform backend."""
|
||||
from mem0_cli.backend.platform import PlatformBackend
|
||||
|
||||
return PlatformBackend(config.platform)
|
||||
@@ -0,0 +1,361 @@
|
||||
"""Platform (SaaS) backend — communicates with api.mem0.ai."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from mem0_cli import __version__
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.config import PlatformConfig
|
||||
|
||||
|
||||
class PlatformBackend(Backend):
|
||||
"""Backend that talks to the mem0 Platform API."""
|
||||
|
||||
def __init__(self, config: PlatformConfig) -> None:
|
||||
self.config = config
|
||||
self.base_url = config.base_url.rstrip("/")
|
||||
self._client = httpx.Client(
|
||||
base_url=self.base_url,
|
||||
headers={
|
||||
"Authorization": f"Token {config.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
"X-Mem0-Client-Version": __version__,
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
||||
from mem0_cli.state import capture_notice, is_agent_mode
|
||||
|
||||
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
||||
resp = self._client.request(method, path, **kwargs)
|
||||
if resp.status_code == 401:
|
||||
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
||||
if resp.status_code == 404:
|
||||
raise NotFoundError(f"Resource not found: {path}")
|
||||
if resp.status_code == 400:
|
||||
# Extract API error detail when available
|
||||
try:
|
||||
detail = resp.json().get("detail", resp.text)
|
||||
except Exception:
|
||||
detail = resp.text
|
||||
raise APIError(f"Bad request to {path}: {detail}")
|
||||
resp.raise_for_status()
|
||||
if resp.status_code == 204:
|
||||
return {}
|
||||
data = resp.json()
|
||||
|
||||
# Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
||||
# fallback for endpoints that return non-dict / non-dict-leading
|
||||
# payloads) and stash it for end-of-command surfacing.
|
||||
notice = None
|
||||
if isinstance(data, dict) and "mem0_notice" in data:
|
||||
notice = data.pop("mem0_notice")
|
||||
elif (
|
||||
isinstance(data, list)
|
||||
and data
|
||||
and isinstance(data[0], dict)
|
||||
and "mem0_notice" in data[0]
|
||||
):
|
||||
notice = data[0].pop("mem0_notice")
|
||||
if notice is None:
|
||||
notice = resp.headers.get("X-Mem0-Notice-Message") or None
|
||||
capture_notice(notice)
|
||||
|
||||
return data
|
||||
|
||||
def add(
|
||||
self,
|
||||
content: str | None = None,
|
||||
messages: list[dict] | None = None,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
immutable: bool = False,
|
||||
infer: bool = True,
|
||||
expires: str | None = None,
|
||||
categories: list[str] | None = None,
|
||||
) -> dict:
|
||||
payload: dict[str, Any] = {}
|
||||
|
||||
if messages:
|
||||
payload["messages"] = messages
|
||||
elif content:
|
||||
payload["messages"] = [{"role": "user", "content": content}]
|
||||
|
||||
if user_id:
|
||||
payload["user_id"] = user_id
|
||||
if agent_id:
|
||||
payload["agent_id"] = agent_id
|
||||
if app_id:
|
||||
payload["app_id"] = app_id
|
||||
if run_id:
|
||||
payload["run_id"] = run_id
|
||||
if metadata:
|
||||
payload["metadata"] = metadata
|
||||
if immutable:
|
||||
payload["immutable"] = True
|
||||
if not infer:
|
||||
payload["infer"] = False
|
||||
if expires:
|
||||
payload["expiration_date"] = expires
|
||||
if categories:
|
||||
payload["categories"] = categories
|
||||
payload["source"] = "CLI"
|
||||
|
||||
return self._request("POST", "/v3/memories/add/", json=payload)
|
||||
|
||||
def _build_filters(
|
||||
self,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
extra_filters: dict | None = None,
|
||||
) -> dict | None:
|
||||
"""Build a filters dict for v3 API endpoints.
|
||||
|
||||
Entity IDs are ANDed (all provided IDs must match).
|
||||
Extra filters (date ranges, categories) are also ANDed.
|
||||
"""
|
||||
# If caller passed a pre-built filter structure (e.g. --filter from CLI), use it directly
|
||||
if extra_filters and ("AND" in extra_filters or "OR" in extra_filters):
|
||||
return extra_filters
|
||||
|
||||
# Build AND conditions for entity IDs
|
||||
and_conditions: list[dict[str, Any]] = []
|
||||
if user_id:
|
||||
and_conditions.append({"user_id": user_id})
|
||||
if agent_id:
|
||||
and_conditions.append({"agent_id": agent_id})
|
||||
if app_id:
|
||||
and_conditions.append({"app_id": app_id})
|
||||
if run_id:
|
||||
and_conditions.append({"run_id": run_id})
|
||||
|
||||
# Append any extra filters (dates, categories)
|
||||
if extra_filters:
|
||||
for k, v in extra_filters.items():
|
||||
and_conditions.append({k: v})
|
||||
|
||||
if len(and_conditions) == 1:
|
||||
return and_conditions[0]
|
||||
elif and_conditions:
|
||||
return {"AND": and_conditions}
|
||||
else:
|
||||
return None
|
||||
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
top_k: int = 10,
|
||||
threshold: float = 0.3,
|
||||
rerank: bool = False,
|
||||
keyword: bool = False,
|
||||
filters: dict | None = None,
|
||||
fields: list[str] | None = None,
|
||||
) -> list[dict]:
|
||||
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
|
||||
|
||||
api_filters = self._build_filters(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
extra_filters=filters,
|
||||
)
|
||||
if api_filters:
|
||||
payload["filters"] = api_filters
|
||||
if rerank:
|
||||
payload["rerank"] = True
|
||||
if keyword:
|
||||
payload["keyword_search"] = True
|
||||
if fields:
|
||||
payload["fields"] = fields
|
||||
payload["source"] = "CLI"
|
||||
|
||||
result = self._request("POST", "/v3/memories/search/", json=payload)
|
||||
return (
|
||||
result
|
||||
if isinstance(result, list)
|
||||
else result.get("results", result.get("memories", []))
|
||||
)
|
||||
|
||||
def get(self, memory_id: str) -> dict:
|
||||
return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||
|
||||
def list_memories(
|
||||
self,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 100,
|
||||
category: str | None = None,
|
||||
after: str | None = None,
|
||||
before: str | None = None,
|
||||
) -> list[dict]:
|
||||
payload: dict[str, Any] = {}
|
||||
params = {"page": str(page), "page_size": str(page_size)}
|
||||
|
||||
# Build filters — entity IDs and date filters go inside "filters"
|
||||
extra: dict[str, Any] = {}
|
||||
if category:
|
||||
extra["categories"] = {"contains": category}
|
||||
if after:
|
||||
extra["created_at"] = {**(extra.get("created_at", {})), "gte": after}
|
||||
if before:
|
||||
extra["created_at"] = {**(extra.get("created_at", {})), "lte": before}
|
||||
|
||||
api_filters = self._build_filters(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
extra_filters=extra if extra else None,
|
||||
)
|
||||
if api_filters:
|
||||
payload["filters"] = api_filters
|
||||
payload["source"] = "CLI"
|
||||
|
||||
result = self._request("POST", "/v3/memories/", json=payload, params=params)
|
||||
return (
|
||||
result
|
||||
if isinstance(result, list)
|
||||
else result.get("results", result.get("memories", []))
|
||||
)
|
||||
|
||||
def update(
|
||||
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||
) -> dict:
|
||||
payload: dict[str, Any] = {}
|
||||
if content:
|
||||
payload["text"] = content
|
||||
if metadata:
|
||||
payload["metadata"] = metadata
|
||||
payload["source"] = "CLI"
|
||||
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
memory_id: str | None = None,
|
||||
*,
|
||||
all: bool = False,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
) -> dict:
|
||||
if all:
|
||||
params: dict[str, str] = {"source": "CLI"}
|
||||
if user_id:
|
||||
params["user_id"] = user_id
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
if app_id:
|
||||
params["app_id"] = app_id
|
||||
if run_id:
|
||||
params["run_id"] = run_id
|
||||
return self._request("DELETE", "/v1/memories/", params=params)
|
||||
elif memory_id:
|
||||
return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||
else:
|
||||
raise ValueError("Either memory_id or --all is required")
|
||||
|
||||
def delete_entities(
|
||||
self,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
) -> dict:
|
||||
# v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||
type_map = {
|
||||
"user": user_id,
|
||||
"agent": agent_id,
|
||||
"app": app_id,
|
||||
"run": run_id,
|
||||
}
|
||||
entities = {t: v for t, v in type_map.items() if v}
|
||||
if not entities:
|
||||
raise ValueError("At least one entity ID is required for delete_entities.")
|
||||
# Delete each provided entity via the v2 path-based endpoint
|
||||
result: dict = {}
|
||||
for entity_type, entity_id in entities.items():
|
||||
result = self._request(
|
||||
"DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
|
||||
)
|
||||
return result
|
||||
|
||||
def ping(self, timeout: float | None = None) -> dict:
|
||||
"""Call the ping endpoint and return the raw response.
|
||||
|
||||
When *timeout* is given it overrides the client-level timeout so that
|
||||
validation pings can fail fast without blocking the user.
|
||||
"""
|
||||
if timeout is not None:
|
||||
resp = self._client.get("/v1/ping/", timeout=timeout)
|
||||
if resp.status_code == 401:
|
||||
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
return self._request("GET", "/v1/ping/")
|
||||
|
||||
def status(
|
||||
self,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Check connectivity using the ping endpoint."""
|
||||
try:
|
||||
self.ping()
|
||||
return {"connected": True, "backend": "platform", "base_url": self.base_url}
|
||||
except Exception as e:
|
||||
return {"connected": False, "backend": "platform", "error": str(e)}
|
||||
|
||||
def entities(self, entity_type: str) -> list[dict]:
|
||||
result = self._request("GET", "/v1/entities/")
|
||||
items = result if isinstance(result, list) else result.get("results", [])
|
||||
# Filter by entity type client-side (API returns all types)
|
||||
type_map = {"users": "user", "agents": "agent", "apps": "app", "runs": "run"}
|
||||
target_type = type_map.get(entity_type)
|
||||
if target_type:
|
||||
items = [e for e in items if e.get("type", "").lower() == target_type]
|
||||
return items
|
||||
|
||||
def list_events(self) -> list[dict]:
|
||||
result = self._request("GET", "/v1/events/")
|
||||
return result if isinstance(result, list) else result.get("results", [])
|
||||
|
||||
def get_event(self, event_id: str) -> dict:
|
||||
return self._request("GET", f"/v1/event/{event_id}/")
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class APIError(Exception):
|
||||
pass
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Branding and ASCII art for mem0 CLI."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.status import Status
|
||||
from rich.text import Text
|
||||
|
||||
# stderr console for spinners, errors, and timing messages
|
||||
_err = Console(stderr=True)
|
||||
|
||||
LOGO = r"""
|
||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
||||
████╗ ████║██╔════╝████╗ ████║██╔═████╗ ██╔════╝██║ ██║
|
||||
██╔████╔██║█████╗ ██╔████╔██║██║██╔██║ ██║ ██║ ██║
|
||||
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║████╔╝██║ ██║ ██║ ██║
|
||||
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚██████╗███████╗██║
|
||||
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝
|
||||
"""
|
||||
|
||||
LOGO_MINI = "◆ mem0"
|
||||
|
||||
TAGLINE = "The Memory Layer for AI Agents"
|
||||
|
||||
BRAND_COLOR = "#8b5cf6" # Purple
|
||||
ACCENT_COLOR = "#a78bfa"
|
||||
SUCCESS_COLOR = "#22c55e"
|
||||
ERROR_COLOR = "#ef4444"
|
||||
WARNING_COLOR = "#f59e0b"
|
||||
DIM_COLOR = "#6b7280"
|
||||
|
||||
|
||||
def _sym(fancy: str, plain: str) -> str:
|
||||
"""Return *fancy* when stdout is a TTY with colour, else *plain*."""
|
||||
if not sys.stdout.isatty() or os.environ.get("NO_COLOR") is not None:
|
||||
return plain
|
||||
return fancy
|
||||
|
||||
|
||||
def print_banner(console: Console) -> None:
|
||||
"""Print the mem0 welcome banner."""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
|
||||
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
|
||||
|
||||
content = Text()
|
||||
content.append_text(logo_text)
|
||||
content.append_text(tagline)
|
||||
|
||||
panel = Panel(
|
||||
content,
|
||||
border_style=BRAND_COLOR,
|
||||
padding=(0, 2),
|
||||
subtitle=f"[{DIM_COLOR}]Python SDK · v{_get_version()}[/]",
|
||||
subtitle_align="right",
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
|
||||
def print_success(console: Console, message: str) -> None:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
sym = _sym("✓", "[ok]")
|
||||
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
|
||||
|
||||
|
||||
def print_error(console: Console, message: str, hint: str | None = None) -> None:
|
||||
from mem0_cli.state import get_current_command, is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
import json as _json
|
||||
|
||||
envelope = {
|
||||
"status": "error",
|
||||
"command": get_current_command(),
|
||||
"error": message,
|
||||
"data": None,
|
||||
}
|
||||
print(_json.dumps(envelope))
|
||||
return
|
||||
from rich.markup import escape
|
||||
|
||||
sym = _sym("✗", "[error]")
|
||||
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {escape(str(message))}")
|
||||
if hint:
|
||||
console.print(f" [{DIM_COLOR}]{escape(str(hint))}[/]")
|
||||
|
||||
|
||||
def print_warning(console: Console, message: str) -> None:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
sym = _sym("⚠", "[warn]")
|
||||
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
|
||||
|
||||
|
||||
def print_info(console: Console, message: str) -> None:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
sym = _sym("◆", "*")
|
||||
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def timed_status(console: Console, message: str):
|
||||
"""Spinner with automatic timing. Yields a context object for setting the final message.
|
||||
|
||||
The spinner and timing output are sent to stderr (via ``_err``) so they
|
||||
never contaminate machine-readable stdout. The *console* parameter is
|
||||
kept for backward compatibility but is not used for spinner output.
|
||||
In agent mode the spinner is suppressed entirely.
|
||||
"""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
class _Ctx:
|
||||
def __init__(self):
|
||||
self.success_msg = ""
|
||||
self.error_msg = ""
|
||||
|
||||
ctx = _Ctx()
|
||||
if is_agent_mode():
|
||||
try:
|
||||
yield ctx
|
||||
except Exception:
|
||||
raise
|
||||
return
|
||||
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
|
||||
yield ctx
|
||||
except Exception:
|
||||
elapsed = time.perf_counter() - start
|
||||
if ctx.error_msg:
|
||||
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
|
||||
if "Authentication failed" in ctx.error_msg:
|
||||
_err.print(
|
||||
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key"
|
||||
f" · [bold]https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/bold][/]"
|
||||
)
|
||||
raise
|
||||
else:
|
||||
elapsed = time.perf_counter() - start
|
||||
if ctx.success_msg:
|
||||
print_success(_err, f"{ctx.success_msg} ({elapsed:.2f}s)")
|
||||
|
||||
|
||||
def print_scope(console: Console, **ids: str | None) -> None:
|
||||
"""Show active entity scope if any IDs are set."""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
parts = []
|
||||
for key, val in ids.items():
|
||||
if val:
|
||||
parts.append(f"{key}={val}")
|
||||
if parts:
|
||||
scope_str = ", ".join(parts)
|
||||
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
|
||||
|
||||
|
||||
def _get_version() -> str:
|
||||
from mem0_cli import __version__
|
||||
|
||||
return __version__
|
||||
@@ -0,0 +1 @@
|
||||
"""CLI command modules."""
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Agent Mode commands — bootstrap (unattended signup) and claim (OTP-based human upgrade)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from mem0_cli.branding import (
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
print_error,
|
||||
print_success,
|
||||
)
|
||||
from mem0_cli.config import Mem0Config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
}
|
||||
|
||||
|
||||
def _validate_envelope(envelope: Any) -> None:
|
||||
"""Defend against partial/malformed backend responses.
|
||||
|
||||
A backend regression that returns ``{"api_key": null}`` would otherwise be
|
||||
silently persisted, producing confusing downstream errors far from the
|
||||
source. Fail fast with a clear message if the required fields are missing.
|
||||
"""
|
||||
if not isinstance(envelope, dict):
|
||||
print_error(err_console, "Bootstrap response was not a JSON object.")
|
||||
raise typer.Exit(1)
|
||||
for field in ("api_key", "default_user_id"):
|
||||
value = envelope.get(field)
|
||||
if not isinstance(value, str) or not value:
|
||||
print_error(
|
||||
err_console,
|
||||
f"Bootstrap response missing required field {field!r} — please update the CLI.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def bootstrap_via_backend(
|
||||
config: Mem0Config,
|
||||
*,
|
||||
source: str | None = None,
|
||||
agent_caller: str | None = None,
|
||||
) -> None:
|
||||
"""POST /api/v1/auth/agent_mode/ and mutate config in place.
|
||||
|
||||
Args:
|
||||
config: Mem0Config mutated in place with the new platform values.
|
||||
source: ``--source`` flag passthrough (analytics tag, free-form).
|
||||
agent_caller: Self-declared agent identity passed via ``--agent-caller``
|
||||
(e.g. ``claude-code``, ``cursor``). May be None when the caller
|
||||
omitted the flag; the agent can backfill later via
|
||||
``mem0 identify <name>``. Sent to the backend in the request body
|
||||
and saved into ``platform.agent_caller`` for local introspection.
|
||||
|
||||
Raises typer.Exit(1) on failure.
|
||||
"""
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
body: dict[str, Any] = {}
|
||||
if source:
|
||||
body["source"] = source
|
||||
if agent_caller:
|
||||
body["agent_caller"] = agent_caller
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.post(
|
||||
f"{base_url}/api/v1/auth/agent_mode/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json=body,
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
print_error(err_console, f"Network error contacting Mem0: {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
if resp.status_code == 429:
|
||||
print_error(err_console, "Rate-limited. Try again in a few minutes.")
|
||||
raise typer.Exit(1)
|
||||
if resp.status_code == 503:
|
||||
print_error(err_console, "Agent Mode is temporarily disabled. Try again later.")
|
||||
raise typer.Exit(1)
|
||||
if resp.status_code != 200:
|
||||
detail = resp.text
|
||||
try:
|
||||
err_body = resp.json()
|
||||
detail = err_body.get("error") or err_body.get("detail") or resp.text
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
pass
|
||||
# Backend's @ratelimit decorator raises PermissionDenied, which DRF
|
||||
# translates to a generic 403 "You do not have permission to perform
|
||||
# this action." That's opaque — surface as the rate-limit it actually is.
|
||||
if resp.status_code == 403 and "permission" in str(detail).lower():
|
||||
print_error(
|
||||
err_console,
|
||||
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
print_error(err_console, f"Bootstrap failed: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
envelope = resp.json()
|
||||
_validate_envelope(envelope)
|
||||
config.platform.api_key = envelope["api_key"]
|
||||
config.platform.base_url = base_url
|
||||
config.platform.agent_mode = True
|
||||
config.platform.created_via = "agent_mode"
|
||||
config.platform.agent_caller = agent_caller or ""
|
||||
config.platform.claimed_at = ""
|
||||
config.platform.default_user_id = envelope["default_user_id"]
|
||||
# Adopt the slug-derived user_id as the default scope for memory ops.
|
||||
config.defaults.user_id = envelope["default_user_id"]
|
||||
save_config(config)
|
||||
|
||||
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
|
||||
notice = envelope.get("mem0_notice")
|
||||
if notice:
|
||||
console.print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
||||
else:
|
||||
# Fallback if the backend hasn't deployed the unified notice yet.
|
||||
claim_cmd = envelope.get("claim_command", "mem0 init --email <your-email>")
|
||||
console.print(f" [{DIM_COLOR}]To claim this account later: {claim_cmd}[/]")
|
||||
|
||||
if not agent_caller:
|
||||
console.print(
|
||||
f" [{DIM_COLOR}]If you are an AI agent, identify yourself: "
|
||||
f"`mem0 identify <your-name>` (e.g. claude-code, cursor).[/]"
|
||||
)
|
||||
|
||||
|
||||
def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None:
|
||||
"""Claim an existing Agent Mode account via OTP — no browser, no polling.
|
||||
|
||||
Reuses the standard email-code flow (`/api/v1/auth/email_code/` then
|
||||
`/.../verify/`) and adds the local agent-mode API key in the verify body
|
||||
as `agent_mode_api_key`. Backend's `verify_email_code` runs the
|
||||
upgrade-in-place transaction inline and returns claim result.
|
||||
|
||||
On success: flips `platform.agent_mode=false`, sets `claimed_at`, stamps
|
||||
`user_email`. The api_key value itself never changes.
|
||||
"""
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
if not config.platform.api_key or not config.platform.agent_mode:
|
||||
print_error(
|
||||
err_console,
|
||||
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
raw_key = config.platform.api_key
|
||||
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
# Step 1: request OTP (unless --code provided)
|
||||
if not code:
|
||||
send = client.post(
|
||||
f"{base_url}/api/v1/auth/email_code/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json={"email": email},
|
||||
)
|
||||
if send.status_code == 429:
|
||||
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||
raise typer.Exit(1)
|
||||
if send.status_code != 200:
|
||||
try:
|
||||
detail = send.json().get("error", send.text)
|
||||
except Exception:
|
||||
detail = send.text
|
||||
print_error(err_console, f"Failed to send code: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
print_success(console, f"Verification code sent to {email}. Check your inbox.")
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
print_error(
|
||||
err_console,
|
||||
"No --code provided and terminal is non-interactive.",
|
||||
hint=f"Re-run: mem0 init --email {email} --code <code>",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
console.print()
|
||||
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
|
||||
if not code:
|
||||
print_error(err_console, "Code is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Step 2: verify + claim in one shot
|
||||
verify = client.post(
|
||||
f"{base_url}/api/v1/auth/email_code/verify/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json={
|
||||
"email": email,
|
||||
"code": code.strip(),
|
||||
"agent_mode_api_key": raw_key,
|
||||
},
|
||||
)
|
||||
|
||||
if verify.status_code != 200:
|
||||
try:
|
||||
err_body = verify.json()
|
||||
detail = err_body.get("error", verify.text)
|
||||
code_str = err_body.get("code", "")
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
detail = verify.text
|
||||
code_str = ""
|
||||
print_error(err_console, f"Claim failed: {detail}")
|
||||
if code_str == "email_already_claimed":
|
||||
console.print(
|
||||
f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in there and run `mem0 link <key>` to attach this agent.[/]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
claim_body = verify.json()
|
||||
if not claim_body.get("claimed"):
|
||||
print_error(err_console, f"Unexpected verify response: {claim_body}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.platform.agent_mode = False
|
||||
config.platform.claimed_at = claim_body.get("claimed_at") or _utcnow_iso()
|
||||
config.platform.user_email = email
|
||||
config.platform.created_via = "email"
|
||||
save_config(config)
|
||||
print_success(console, f"Agent claimed to {email}. Your API key is unchanged.")
|
||||
|
||||
|
||||
def _utcnow_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Config management commands: show, set, get."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, print_error, print_success
|
||||
from mem0_cli.config import (
|
||||
get_nested_value,
|
||||
load_config,
|
||||
redact_key,
|
||||
save_config,
|
||||
set_nested_value,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def cmd_config_show(*, output: str = "text") -> None:
|
||||
"""Display current configuration (secrets redacted)."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("config show")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
|
||||
config = load_config()
|
||||
|
||||
if output in ("json", "agent"):
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="config show",
|
||||
data={
|
||||
"defaults": {
|
||||
"user_id": config.defaults.user_id or None,
|
||||
"agent_id": config.defaults.agent_id or None,
|
||||
"app_id": config.defaults.app_id or None,
|
||||
"run_id": config.defaults.run_id or None,
|
||||
},
|
||||
"platform": {
|
||||
"api_key": redact_key(config.platform.api_key),
|
||||
"base_url": config.platform.base_url,
|
||||
},
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
console.print()
|
||||
console.print(f" [{BRAND_COLOR}]◆ mem0 Configuration[/]\n")
|
||||
|
||||
table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 2))
|
||||
table.add_column("Key", style="bold")
|
||||
table.add_column("Value")
|
||||
|
||||
# Defaults
|
||||
table.add_row(
|
||||
"defaults.user_id",
|
||||
config.defaults.user_id or f"[{DIM_COLOR}](not set)[/]",
|
||||
)
|
||||
table.add_row(
|
||||
"defaults.agent_id",
|
||||
config.defaults.agent_id or f"[{DIM_COLOR}](not set)[/]",
|
||||
)
|
||||
table.add_row(
|
||||
"defaults.app_id",
|
||||
config.defaults.app_id or f"[{DIM_COLOR}](not set)[/]",
|
||||
)
|
||||
table.add_row(
|
||||
"defaults.run_id",
|
||||
config.defaults.run_id or f"[{DIM_COLOR}](not set)[/]",
|
||||
)
|
||||
table.add_row("", "")
|
||||
|
||||
# Platform
|
||||
table.add_row("[bold]platform.api_key[/]", redact_key(config.platform.api_key))
|
||||
table.add_row("platform.base_url", config.platform.base_url)
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_config_get(key: str) -> None:
|
||||
"""Get a config value."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("config get")
|
||||
config = load_config()
|
||||
value = get_nested_value(config, key)
|
||||
|
||||
if value is None:
|
||||
print_error(err_console, f"Unknown config key: {key}")
|
||||
return
|
||||
|
||||
display_value = (
|
||||
redact_key(str(value)) if ("api_key" in key or "key" in key.split(".")[-1:]) else str(value)
|
||||
)
|
||||
|
||||
if is_agent_mode():
|
||||
format_agent_envelope(
|
||||
console, command="config get", data={"key": key, "value": display_value}
|
||||
)
|
||||
else:
|
||||
console.print(display_value)
|
||||
|
||||
|
||||
def cmd_config_set(key: str, value: str) -> None:
|
||||
"""Set a config value."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("config set")
|
||||
config = load_config()
|
||||
if set_nested_value(config, key, value):
|
||||
save_config(config)
|
||||
display = redact_key(value) if "key" in key else value
|
||||
if is_agent_mode():
|
||||
format_agent_envelope(
|
||||
console, command="config set", data={"key": key, "value": display}
|
||||
)
|
||||
else:
|
||||
print_success(console, f"{key} = {display}")
|
||||
else:
|
||||
print_error(err_console, f"Unknown config key: {key}")
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Entity management commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time as _time
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.branding import (
|
||||
ACCENT_COLOR,
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
print_error,
|
||||
print_info,
|
||||
print_success,
|
||||
timed_status,
|
||||
)
|
||||
from mem0_cli.output import format_agent_envelope, format_json
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
|
||||
"""List entities of a given type."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("entity list")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
valid_types = {"users", "agents", "apps", "runs"}
|
||||
if entity_type not in valid_types:
|
||||
print_error(
|
||||
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, f"Fetching {entity_type}...") as _ts:
|
||||
try:
|
||||
results = backend.entities(entity_type)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e), hint="This feature may require the mem0 Platform.")
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="entity list",
|
||||
data=results,
|
||||
count=len(results),
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
return
|
||||
|
||||
if not results:
|
||||
print_info(console, f"No {entity_type} found.")
|
||||
return
|
||||
|
||||
table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 1))
|
||||
table.add_column("Name / ID", style="bold")
|
||||
table.add_column("Created", max_width=12)
|
||||
|
||||
for entity in results:
|
||||
name = entity.get("name", entity.get("id", "—"))
|
||||
created = str(entity.get("created_at", "—"))[:10]
|
||||
table.add_row(str(name), created)
|
||||
|
||||
console.print()
|
||||
console.print(table)
|
||||
console.print(f" [{DIM_COLOR}]{len(results)} {entity_type} ({_elapsed:.2f}s)[/]")
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_entities_delete(
|
||||
backend: Backend,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
force: bool,
|
||||
dry_run: bool = False,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete an entity and all its memories (cascade delete)."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("entity delete")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
if not force:
|
||||
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||
raise typer.Exit(1)
|
||||
if not any([user_id, agent_id, app_id, run_id]):
|
||||
print_error(
|
||||
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
scope_parts = []
|
||||
if user_id:
|
||||
scope_parts.append(f"user={user_id}")
|
||||
if agent_id:
|
||||
scope_parts.append(f"agent={agent_id}")
|
||||
if app_id:
|
||||
scope_parts.append(f"app={app_id}")
|
||||
if run_id:
|
||||
scope_parts.append(f"run={run_id}")
|
||||
scope_str = ", ".join(scope_parts)
|
||||
|
||||
if dry_run:
|
||||
print_info(console, f"Would delete entity {scope_str} and all its memories.")
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
if not force:
|
||||
confirm = typer.confirm(
|
||||
f"\n \u26a0 Delete entity {scope_str} AND all its memories? This cannot be undone."
|
||||
)
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled.")
|
||||
raise typer.Exit(0)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting entity...") as _ts:
|
||||
try:
|
||||
result = backend.delete_entities(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="entity delete",
|
||||
data={"deleted": True},
|
||||
scope=scope or None,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Event commands: list and status."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.branding import (
|
||||
ACCENT_COLOR,
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
ERROR_COLOR,
|
||||
SUCCESS_COLOR,
|
||||
WARNING_COLOR,
|
||||
print_info,
|
||||
timed_status,
|
||||
)
|
||||
from mem0_cli.output import format_agent_envelope, format_json
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_STATUS_STYLE = {
|
||||
"SUCCEEDED": f"[{SUCCESS_COLOR}]SUCCEEDED[/]",
|
||||
"PENDING": f"[{ACCENT_COLOR}]PENDING[/]",
|
||||
"FAILED": f"[{ERROR_COLOR}]FAILED[/]",
|
||||
"PROCESSING": f"[{WARNING_COLOR}]PROCESSING[/]",
|
||||
}
|
||||
|
||||
|
||||
def _status_styled(status: str) -> str:
|
||||
return _STATUS_STYLE.get(status.upper(), status)
|
||||
|
||||
|
||||
def cmd_event_list(backend: Backend, *, output: str = "table") -> None:
|
||||
"""List recent background events."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("event list")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
import time as _time
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Fetching events...") as _ts:
|
||||
try:
|
||||
results = backend.list_events()
|
||||
except Exception as e:
|
||||
_ts.error_msg = str(e)
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="event list",
|
||||
data=results,
|
||||
count=len(results),
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
return
|
||||
|
||||
if not results:
|
||||
console.print()
|
||||
print_info(console, "No events found.")
|
||||
console.print()
|
||||
return
|
||||
|
||||
table = Table(
|
||||
border_style=BRAND_COLOR,
|
||||
header_style=f"bold {ACCENT_COLOR}",
|
||||
row_styles=["", "dim"],
|
||||
padding=(0, 1),
|
||||
)
|
||||
table.add_column("Event ID", style="dim", max_width=10, no_wrap=True)
|
||||
table.add_column("Type", max_width=14)
|
||||
table.add_column("Status", max_width=12)
|
||||
table.add_column("Latency", max_width=10, justify="right")
|
||||
table.add_column("Created", max_width=20)
|
||||
|
||||
for ev in results:
|
||||
ev_id = str(ev.get("id", ""))[:8]
|
||||
ev_type = str(ev.get("event_type", "—"))
|
||||
status = str(ev.get("status", "—"))
|
||||
latency = ev.get("latency")
|
||||
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||
table.add_row(ev_id, ev_type, _status_styled(status), latency_str, created)
|
||||
|
||||
console.print()
|
||||
console.print(table)
|
||||
console.print(f" [{DIM_COLOR}]{len(results)} event{'s' if len(results) != 1 else ''}[/]")
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_event_status(backend: Backend, event_id: str, *, output: str = "text") -> None:
|
||||
"""Get the status of a specific background event."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("event status")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
import time as _time
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Fetching event...") as _ts:
|
||||
try:
|
||||
ev = backend.get_event(event_id)
|
||||
except Exception as e:
|
||||
_ts.error_msg = str(e)
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="event status",
|
||||
data=ev,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, ev)
|
||||
return
|
||||
|
||||
status = str(ev.get("status", "—"))
|
||||
ev_type = str(ev.get("event_type", "—"))
|
||||
latency = ev.get("latency")
|
||||
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||
updated = str(ev.get("updated_at", "—"))[:19].replace("T", " ")
|
||||
results = ev.get("results")
|
||||
|
||||
lines = []
|
||||
lines.append(f" [{DIM_COLOR}]Event ID:[/] {event_id}")
|
||||
lines.append(f" [{DIM_COLOR}]Type:[/] {ev_type}")
|
||||
lines.append(f" [{DIM_COLOR}]Status:[/] {_status_styled(status)}")
|
||||
lines.append(f" [{DIM_COLOR}]Latency:[/] {latency_str}")
|
||||
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
|
||||
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
|
||||
|
||||
if results:
|
||||
lines.append("")
|
||||
lines.append(f" [{DIM_COLOR}]Results ({len(results)}):[/]")
|
||||
for r in results:
|
||||
mem_id = str(r.get("id", ""))[:8]
|
||||
data = r.get("data", {})
|
||||
memory = data.get("memory", "") if isinstance(data, dict) else str(data)
|
||||
ev_name = str(r.get("event", ""))
|
||||
user = str(r.get("user_id", ""))
|
||||
detail = f"{ev_name} {memory}"
|
||||
if user:
|
||||
detail += f" [{DIM_COLOR}](user_id={user})[/]"
|
||||
lines.append(f" [{SUCCESS_COLOR}]·[/] {detail} [{DIM_COLOR}]({mem_id})[/]")
|
||||
|
||||
content = "\n".join(lines)
|
||||
panel = Panel(
|
||||
content,
|
||||
title=f"[{BRAND_COLOR}]Event Status[/]",
|
||||
title_align="left",
|
||||
border_style=BRAND_COLOR,
|
||||
padding=(1, 1),
|
||||
)
|
||||
console.print()
|
||||
console.print(panel)
|
||||
console.print()
|
||||
@@ -0,0 +1,75 @@
|
||||
"""mem0 identify — declare which agent owns the current agent-mode key.
|
||||
|
||||
Used when `mem0 init --agent` ran without --agent-caller, so the backend
|
||||
saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
|
||||
its own row with its real identity. Idempotent — running it again just
|
||||
overwrites.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.branding import print_error, print_success
|
||||
from mem0_cli.config import load_config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
}
|
||||
|
||||
|
||||
def run_identify(name: str) -> None:
|
||||
"""PATCH the active agent-mode key's agent_caller field."""
|
||||
config = load_config()
|
||||
if not config.platform.api_key:
|
||||
print_error(
|
||||
err_console,
|
||||
"No API key configured. Run `mem0 init --agent` first.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
if not config.platform.agent_mode:
|
||||
print_error(
|
||||
err_console,
|
||||
"This command only works on unclaimed agent-mode keys.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
print_error(err_console, "Agent name is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.patch(
|
||||
f"{base_url}/api/v1/auth/agent_mode/caller/",
|
||||
headers={
|
||||
**_SOURCE_HEADERS,
|
||||
"Authorization": f"Token {config.platform.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"agent_caller": name},
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
print_error(err_console, f"Network error: {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
if resp.status_code != 200:
|
||||
try:
|
||||
detail = resp.json().get("error", resp.text)
|
||||
except Exception:
|
||||
detail = resp.text
|
||||
print_error(err_console, f"Identify failed: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
canonical = resp.json().get("agent_caller", name)
|
||||
config.platform.agent_caller = canonical
|
||||
save_config(config)
|
||||
print_success(console, f"Identified as {canonical}.")
|
||||
@@ -0,0 +1,566 @@
|
||||
"""mem0 init — interactive setup wizard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from mem0_cli.branding import (
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
print_banner,
|
||||
print_error,
|
||||
print_info,
|
||||
print_success,
|
||||
)
|
||||
from mem0_cli.config import (
|
||||
CONFIG_FILE,
|
||||
DEFAULT_BASE_URL,
|
||||
Mem0Config,
|
||||
load_config,
|
||||
save_config,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def _prompt_secret(label: str) -> str:
|
||||
"""Prompt for a secret value, echoing '*' for each character typed."""
|
||||
sys.stdout.write(label)
|
||||
sys.stdout.flush()
|
||||
|
||||
chars: list[str] = []
|
||||
|
||||
if sys.platform == "win32":
|
||||
import msvcrt
|
||||
|
||||
while True:
|
||||
ch = msvcrt.getwch()
|
||||
if ch in ("\r", "\n"):
|
||||
sys.stdout.write("\n")
|
||||
sys.stdout.flush()
|
||||
break
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt
|
||||
if ch in ("\x08", "\x7f"): # backspace
|
||||
if chars:
|
||||
chars.pop()
|
||||
sys.stdout.write("\b \b")
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
chars.append(ch)
|
||||
sys.stdout.write("*")
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
import termios
|
||||
import tty
|
||||
|
||||
fd = sys.stdin.fileno()
|
||||
old_settings = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
while True:
|
||||
ch = sys.stdin.read(1)
|
||||
if ch in ("\r", "\n"):
|
||||
sys.stdout.write("\r\n")
|
||||
sys.stdout.flush()
|
||||
break
|
||||
if ch == "\x03":
|
||||
raise KeyboardInterrupt
|
||||
if ch in ("\x7f", "\x08"): # backspace/delete
|
||||
if chars:
|
||||
chars.pop()
|
||||
sys.stdout.write("\b \b")
|
||||
sys.stdout.flush()
|
||||
elif ch == "\x15": # Ctrl+U — clear line
|
||||
sys.stdout.write("\b \b" * len(chars))
|
||||
sys.stdout.flush()
|
||||
chars = []
|
||||
elif ch >= " ": # ignore other control characters
|
||||
chars.append(ch)
|
||||
sys.stdout.write("*")
|
||||
sys.stdout.flush()
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||
|
||||
return "".join(chars)
|
||||
|
||||
|
||||
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||
|
||||
|
||||
def _validate_email(email: str) -> None:
|
||||
"""Exit with an error if *email* doesn't look like a valid address."""
|
||||
if not _EMAIL_RE.match(email):
|
||||
print_error(err_console, f"Invalid email address: {email!r}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _ping_key(api_key: str, base_url: str, timeout: float = 5.0) -> bool:
|
||||
"""Validate api_key against /v1/ping/.
|
||||
|
||||
Returns False ONLY on a definitive "invalid key" signal (HTTP 401 / 403).
|
||||
Network errors, timeouts, and 5xx responses return True so we prefer
|
||||
reusing an existing key over silently minting a new shadow on a transient
|
||||
blip (which would also clobber config + plugin-sync targets).
|
||||
"""
|
||||
try:
|
||||
resp = httpx.get(
|
||||
f"{base_url.rstrip('/')}/v1/ping/",
|
||||
headers={"Authorization": f"Token {api_key}"},
|
||||
timeout=timeout,
|
||||
)
|
||||
except httpx.HTTPError:
|
||||
return True # unknown — prefer reuse
|
||||
return resp.status_code not in (401, 403)
|
||||
|
||||
|
||||
def _email_login(
|
||||
email: str,
|
||||
code: str | None,
|
||||
base_url: str,
|
||||
) -> dict:
|
||||
"""Run the email verification code login flow.
|
||||
|
||||
Returns the parsed JSON response from the verify endpoint.
|
||||
The caller expects at minimum an ``api_key`` field.
|
||||
"""
|
||||
url = base_url.rstrip("/")
|
||||
_source_headers = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
# If code is already provided, skip sending — user already has a code
|
||||
if not code:
|
||||
# Step 1: Request verification code
|
||||
resp = client.post(
|
||||
f"{url}/api/v1/auth/email_code/",
|
||||
json={"email": email},
|
||||
headers=_source_headers,
|
||||
)
|
||||
if resp.status_code == 429:
|
||||
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||
raise typer.Exit(1)
|
||||
if resp.status_code != 200:
|
||||
try:
|
||||
detail = resp.json().get("error", resp.text)
|
||||
except Exception:
|
||||
detail = resp.text
|
||||
print_error(err_console, f"Failed to send code: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
print_success(console, "Verification code sent! Check your email.")
|
||||
|
||||
# Step 2: Get code from user
|
||||
if not sys.stdin.isatty():
|
||||
print_error(
|
||||
err_console,
|
||||
"No --code provided and terminal is non-interactive.",
|
||||
hint="Run: mem0 init --email <email> --code <code>",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
console.print()
|
||||
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
|
||||
if not code:
|
||||
print_error(err_console, "Code is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Step 3: Verify code
|
||||
resp = client.post(
|
||||
f"{url}/api/v1/auth/email_code/verify/",
|
||||
json={"email": email, "code": code.strip()},
|
||||
headers=_source_headers,
|
||||
)
|
||||
if resp.status_code == 429:
|
||||
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||
raise typer.Exit(1)
|
||||
if resp.status_code != 200:
|
||||
try:
|
||||
detail = resp.json().get("error", resp.text)
|
||||
except Exception:
|
||||
detail = resp.text
|
||||
print_error(err_console, f"Verification failed: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
return resp.json()
|
||||
|
||||
|
||||
def run_init(
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
user_id: str | None = None,
|
||||
email: str | None = None,
|
||||
code: str | None = None,
|
||||
force: bool = False,
|
||||
source: str | None = None,
|
||||
agent: bool = False,
|
||||
agent_caller: str | None = None,
|
||||
) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||
(non-interactive mode). When running in a non-TTY without the required
|
||||
flags, an error message is printed.
|
||||
|
||||
Agent Mode dispatch (no email/api-key flags):
|
||||
- If existing config has an active API key → reuse (existing_key path).
|
||||
- Else if any positive agent signal (--agent, --json global, agent env
|
||||
var, or `agent` flag) → POST /api/v1/auth/agent_mode/ and write config.
|
||||
- Else fall through to the interactive wizard.
|
||||
|
||||
Claim dispatch:
|
||||
- If `--email` is set AND existing config has `agent_mode=true`, run the
|
||||
claim device-flow against the existing key instead of minting a new
|
||||
email-based key.
|
||||
"""
|
||||
from mem0_cli.agent_detect import detect_agent_caller
|
||||
from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend, claim_via_otp
|
||||
from mem0_cli.state import is_agent_mode as _global_agent_mode
|
||||
from mem0_cli.telemetry import capture_event
|
||||
|
||||
def _fire_init(mode: str, *, claimed: bool = False) -> None:
|
||||
"""Fire cli.init telemetry with M1-M6 properties."""
|
||||
props: dict = {"command": "init", "mode": mode}
|
||||
if agent_caller:
|
||||
# Self-declared via --agent-caller; not sniffed from env vars.
|
||||
props["agent_caller"] = agent_caller
|
||||
if source:
|
||||
props["signup_source"] = source
|
||||
if claimed:
|
||||
props["claimed_agent_mode"] = True
|
||||
capture_event("cli.init", props)
|
||||
|
||||
config = Mem0Config()
|
||||
|
||||
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
|
||||
config.platform.base_url = base_url
|
||||
|
||||
if code and not email:
|
||||
print_error(err_console, "--code requires --email.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# ── Email + existing agent-mode config → claim flow ─────────────────
|
||||
if email and CONFIG_FILE.exists():
|
||||
existing = load_config()
|
||||
if existing.platform.agent_mode and existing.platform.api_key:
|
||||
email = email.strip().lower()
|
||||
_validate_email(email)
|
||||
print_info(console, f"Claiming Agent Mode account to {email}...")
|
||||
claim_via_otp(existing, email=email, code=code)
|
||||
_fire_init("email", claimed=True)
|
||||
return
|
||||
|
||||
# ── Agent Mode path runs BEFORE the existing-config guard ──────────
|
||||
# Rules 1/2 REUSE a valid existing key (not overwrite), so we must
|
||||
# short-circuit before the guard prompts. Rule 3 mints only when there
|
||||
# is no valid key to reuse — in that case overwriting is correct.
|
||||
_agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None)
|
||||
if not api_key and not email and _agent_ctx:
|
||||
from mem0_cli.output import format_json_envelope
|
||||
from mem0_cli.state import is_agent_mode as _is_json_mode
|
||||
|
||||
def _emit_reuse(source: str) -> None:
|
||||
if _is_json_mode():
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="init",
|
||||
data={
|
||||
"api_key_saved": False,
|
||||
"api_key_source": source,
|
||||
"agent_mode": False,
|
||||
"message": "Existing Mem0 API key found and reused. No Agent Mode key was created.",
|
||||
},
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
"Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
|
||||
if source == "env"
|
||||
else "Existing API key in config is valid; reusing it. No new Agent Mode key was minted."
|
||||
)
|
||||
print_success(console, msg)
|
||||
|
||||
def _maybe_identify(key: str) -> None:
|
||||
"""Best-effort PATCH agent_caller when --agent-caller is supplied on a
|
||||
reused key. Silent no-op on any failure — reuse must not break.
|
||||
"""
|
||||
if not agent_caller:
|
||||
return
|
||||
try:
|
||||
resp = httpx.patch(
|
||||
f"{base_url.rstrip('/')}/api/v1/auth/agent_mode/caller/",
|
||||
headers={
|
||||
"Authorization": f"Token {key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"agent_caller": agent_caller},
|
||||
timeout=10.0,
|
||||
)
|
||||
# Also reflect in local config so introspection matches backend.
|
||||
if resp.status_code == 200 and CONFIG_FILE.exists():
|
||||
try:
|
||||
cfg = load_config()
|
||||
cfg.platform.agent_caller = resp.json().get("agent_caller", agent_caller)
|
||||
save_config(cfg)
|
||||
except Exception:
|
||||
pass
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
|
||||
# Rule 1: env MEM0_API_KEY valid → reuse, no new key.
|
||||
_env_key = (os.environ.get("MEM0_API_KEY") or "").strip()
|
||||
if _env_key and _ping_key(_env_key, base_url):
|
||||
_maybe_identify(_env_key)
|
||||
_emit_reuse("env")
|
||||
_fire_init("existing_key")
|
||||
return
|
||||
# Rule 2: existing config api_key valid → reuse.
|
||||
if CONFIG_FILE.exists():
|
||||
_existing = load_config()
|
||||
if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url):
|
||||
_maybe_identify(_existing.platform.api_key)
|
||||
_emit_reuse("config")
|
||||
_fire_init("existing_key")
|
||||
return
|
||||
# Rule 3: mint a fresh shadow (no valid key to reuse).
|
||||
# agent_caller is the agent's self-declared identity from --agent-caller
|
||||
# (Proof Editor-style). Env-var auto-detect is still used above to
|
||||
# decide we're in an agent context, but never to fill identity.
|
||||
bootstrap_via_backend(config, source=source, agent_caller=agent_caller)
|
||||
_fire_init("agent")
|
||||
return
|
||||
|
||||
# Warn if an existing config with an API key would be overwritten
|
||||
if not force and CONFIG_FILE.exists():
|
||||
existing = load_config()
|
||||
if existing.platform.api_key:
|
||||
from mem0_cli.config import redact_key
|
||||
|
||||
console.print(
|
||||
f"\n [{BRAND_COLOR}]Existing configuration found[/] "
|
||||
f"[{DIM_COLOR}](API key: {redact_key(existing.platform.api_key)})[/]"
|
||||
)
|
||||
if sys.stdin.isatty():
|
||||
confirm = typer.confirm(" Overwrite existing config? This cannot be undone.")
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled. Use --force to skip this check.")
|
||||
raise typer.Exit(0)
|
||||
else:
|
||||
print_error(
|
||||
err_console,
|
||||
"Existing config would be overwritten.",
|
||||
hint="Use --force to overwrite.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# ── Email login flow ──────────────────────────────────────────────
|
||||
if email:
|
||||
if api_key:
|
||||
print_error(err_console, "Cannot use both --api-key and --email.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
email = email.strip().lower()
|
||||
_validate_email(email)
|
||||
|
||||
print_banner(console)
|
||||
console.print()
|
||||
print_info(console, f"Logging in as {email}...\n")
|
||||
|
||||
result = _email_login(email, code, base_url)
|
||||
|
||||
api_key_val = result.get("api_key")
|
||||
if not api_key_val:
|
||||
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
|
||||
raise typer.Exit(1)
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
config.platform.user_email = email
|
||||
config.platform.created_via = "email"
|
||||
config.defaults.user_id = (
|
||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
)
|
||||
|
||||
save_config(config)
|
||||
|
||||
console.print()
|
||||
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
|
||||
console.print()
|
||||
console.print(f" [{DIM_COLOR}]Get started:[/]")
|
||||
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
|
||||
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
|
||||
console.print()
|
||||
return
|
||||
|
||||
# ── API key flow (existing) ───────────────────────────────────────
|
||||
# (Agent Mode branch runs earlier — see above, before the existing-config
|
||||
# guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
|
||||
|
||||
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||
if not sys.stdin.isatty():
|
||||
if not api_key:
|
||||
print_error(
|
||||
err_console,
|
||||
"Non-interactive terminal detected and --api-key is required.",
|
||||
hint="Run: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
|
||||
# Fully non-interactive when both flags provided
|
||||
if api_key and user_id:
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
config.defaults.user_id = user_id
|
||||
_validate_platform(config)
|
||||
save_config(config)
|
||||
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||
return
|
||||
|
||||
print_banner(console)
|
||||
console.print()
|
||||
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
|
||||
|
||||
# If no flags at all, ask user how they want to authenticate
|
||||
if not api_key:
|
||||
console.print(f" [{BRAND_COLOR}]How would you like to authenticate?[/]")
|
||||
console.print(f" [{DIM_COLOR}]1.[/] Login with email [{DIM_COLOR}](recommended)[/]")
|
||||
console.print(f" [{DIM_COLOR}]2.[/] Enter API key manually")
|
||||
console.print()
|
||||
choice = Prompt.ask(f" [{BRAND_COLOR}]Choose[/]", choices=["1", "2"], default="1")
|
||||
|
||||
if choice == "1":
|
||||
console.print()
|
||||
email_addr = Prompt.ask(f" [{BRAND_COLOR}]Email[/]")
|
||||
if not email_addr:
|
||||
print_error(err_console, "Email is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
email_addr = email_addr.strip().lower()
|
||||
_validate_email(email_addr)
|
||||
print_info(console, f"Logging in as {email_addr}...\n")
|
||||
|
||||
result = _email_login(email_addr, None, base_url)
|
||||
|
||||
api_key_val = result.get("api_key")
|
||||
if not api_key_val:
|
||||
print_error(
|
||||
err_console, "Auth succeeded but no API key was returned. Contact support."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
config.platform.user_email = email_addr
|
||||
config.platform.created_via = "email"
|
||||
config.defaults.user_id = (
|
||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
)
|
||||
|
||||
save_config(config)
|
||||
|
||||
console.print()
|
||||
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
|
||||
console.print()
|
||||
console.print(f" [{DIM_COLOR}]Get started:[/]")
|
||||
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
|
||||
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
|
||||
console.print()
|
||||
return
|
||||
|
||||
# API key flow
|
||||
if api_key:
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
else:
|
||||
_setup_platform(config)
|
||||
|
||||
if user_id:
|
||||
config.defaults.user_id = user_id
|
||||
else:
|
||||
_setup_defaults(config)
|
||||
|
||||
_validate_platform(config)
|
||||
|
||||
save_config(config)
|
||||
console.print()
|
||||
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||
console.print()
|
||||
console.print(f" [{DIM_COLOR}]Get started:[/]")
|
||||
if config.defaults.user_id:
|
||||
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
|
||||
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
|
||||
else:
|
||||
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode" --user-id alice[/]')
|
||||
console.print(f' [{DIM_COLOR}] mem0 search "preferences" --user-id alice[/]')
|
||||
console.print()
|
||||
|
||||
|
||||
def _setup_platform(config: Mem0Config) -> None:
|
||||
"""Platform setup flow."""
|
||||
console.print()
|
||||
console.print(
|
||||
f" [{DIM_COLOR}]Get your API key at https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/]"
|
||||
)
|
||||
console.print()
|
||||
|
||||
console.print(f" [{BRAND_COLOR}]API Key[/]: ", end="")
|
||||
api_key = _prompt_secret("")
|
||||
if not api_key:
|
||||
print_error(err_console, "API key is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
|
||||
|
||||
def _setup_defaults(config: Mem0Config) -> None:
|
||||
"""Collect default entity IDs."""
|
||||
console.print()
|
||||
print_info(console, "Set default entity IDs (press Enter to skip).\n")
|
||||
|
||||
_default_user = os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
user_id = Prompt.ask(
|
||||
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
|
||||
default=_default_user,
|
||||
)
|
||||
if user_id:
|
||||
config.defaults.user_id = user_id
|
||||
|
||||
|
||||
def _validate_platform(config: Mem0Config) -> None:
|
||||
"""Validate platform connection after all inputs are collected."""
|
||||
console.print()
|
||||
print_info(console, "Validating connection...")
|
||||
try:
|
||||
from mem0_cli.backend.platform import PlatformBackend
|
||||
|
||||
backend = PlatformBackend(config.platform)
|
||||
status = backend.status(
|
||||
user_id=config.defaults.user_id or None,
|
||||
agent_id=config.defaults.agent_id or None,
|
||||
)
|
||||
if status.get("connected"):
|
||||
print_success(console, "Connected to mem0 Platform!")
|
||||
# Cache user_email from ping response for telemetry distinct_id
|
||||
try:
|
||||
ping_data = backend.ping()
|
||||
user_email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
|
||||
if user_email:
|
||||
config.platform.user_email = user_email
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
print_error(
|
||||
err_console,
|
||||
f"Could not connect: {status.get('error', 'Unknown error')}",
|
||||
hint="Visit https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python to get a new key, then run mem0 init again.",
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, f"Connection test failed: {e}")
|
||||
@@ -0,0 +1,671 @@
|
||||
"""Memory CRUD commands: add, search, get, list, update, delete."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat as _stat_mod
|
||||
import sys
|
||||
import time as _time
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.branding import (
|
||||
print_error,
|
||||
print_info,
|
||||
print_scope,
|
||||
print_success,
|
||||
timed_status,
|
||||
)
|
||||
from mem0_cli.output import (
|
||||
format_add_result,
|
||||
format_agent_envelope,
|
||||
format_json,
|
||||
format_memories_table,
|
||||
format_memories_text,
|
||||
format_single_memory,
|
||||
print_result_summary,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def _stdin_is_piped() -> bool:
|
||||
"""Return True only when stdin is an actual pipe or file redirect."""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return False
|
||||
try:
|
||||
mode = os.fstat(sys.stdin.fileno()).st_mode
|
||||
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def cmd_add(
|
||||
backend: Backend,
|
||||
text: str | None,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
messages: str | None,
|
||||
file: Path | None,
|
||||
metadata: str | None,
|
||||
immutable: bool,
|
||||
no_infer: bool,
|
||||
expires: str | None,
|
||||
categories: str | None,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Add a memory."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("add")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
msgs = None
|
||||
content = text
|
||||
|
||||
# Read from file
|
||||
if file:
|
||||
try:
|
||||
raw = Path(file).read_text()
|
||||
msgs = json.loads(raw)
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
print_error(err_console, f"Failed to read file: {e}")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Parse messages JSON
|
||||
elif messages:
|
||||
try:
|
||||
msgs = json.loads(messages)
|
||||
except json.JSONDecodeError as e:
|
||||
print_error(err_console, f"Invalid JSON in --messages: {e}")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Read from stdin only if stdin is an actual pipe or file redirect
|
||||
elif not content and _stdin_is_piped():
|
||||
content = sys.stdin.read().strip()
|
||||
|
||||
if not content and not msgs:
|
||||
print_error(
|
||||
err_console, "No content provided. Pass text, --messages, --file, or pipe via stdin."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
meta = None
|
||||
if metadata:
|
||||
try:
|
||||
meta = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
print_error(err_console, "Invalid JSON in --metadata.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
cats = None
|
||||
if categories:
|
||||
try:
|
||||
cats = json.loads(categories)
|
||||
except json.JSONDecodeError:
|
||||
cats = [c.strip() for c in categories.split(",")]
|
||||
|
||||
# Validate --expires
|
||||
if expires:
|
||||
import re
|
||||
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
|
||||
print_error(
|
||||
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
from datetime import date
|
||||
|
||||
if date.fromisoformat(expires) <= date.today():
|
||||
print_error(err_console, "--expires date must be in the future.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
with timed_status(err_console, "Adding memory...") as ts:
|
||||
try:
|
||||
result = backend.add(
|
||||
content=content,
|
||||
messages=msgs,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
metadata=meta,
|
||||
immutable=immutable,
|
||||
infer=not no_infer,
|
||||
expires=expires,
|
||||
categories=cats,
|
||||
)
|
||||
except Exception as e:
|
||||
ts.error_msg = str(e)
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
# Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||
results_list = result if isinstance(result, list) else result.get("results", [result])
|
||||
seen_events: set[str] = set()
|
||||
deduped: list[dict] = []
|
||||
for r in results_list:
|
||||
if r.get("status") == "PENDING":
|
||||
eid = r.get("event_id", "")
|
||||
if eid and eid in seen_events:
|
||||
continue
|
||||
if eid:
|
||||
seen_events.add(eid)
|
||||
deduped.append(r)
|
||||
# Write back so downstream formatters see deduplicated data
|
||||
if isinstance(result, dict) and "results" in result:
|
||||
result = {**result, "results": deduped}
|
||||
else:
|
||||
result = deduped
|
||||
|
||||
if output == "agent":
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="add",
|
||||
data=deduped,
|
||||
scope=scope or None,
|
||||
count=len(deduped),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_add_result(console, result, output)
|
||||
return
|
||||
|
||||
console.print()
|
||||
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
||||
count = len(deduped)
|
||||
all_pending = count > 0 and all(r.get("status") == "PENDING" for r in deduped)
|
||||
if all_pending:
|
||||
print_success(
|
||||
console,
|
||||
f"Memory queued — {count} event{'s' if count != 1 else ''} pending",
|
||||
)
|
||||
else:
|
||||
print_success(
|
||||
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
|
||||
)
|
||||
format_add_result(console, result, output)
|
||||
|
||||
|
||||
def cmd_search(
|
||||
backend: Backend,
|
||||
query: str,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
top_k: int,
|
||||
threshold: float,
|
||||
rerank: bool,
|
||||
keyword: bool,
|
||||
filter_json: str | None,
|
||||
fields: str | None,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Search memories."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("search")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
filters = None
|
||||
if filter_json:
|
||||
try:
|
||||
filters = json.loads(filter_json)
|
||||
except json.JSONDecodeError:
|
||||
print_error(err_console, "Invalid JSON in --filter.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
field_list = None
|
||||
if fields:
|
||||
field_list = [f.strip() for f in fields.split(",")]
|
||||
|
||||
if top_k < 1:
|
||||
print_error(err_console, "--top-k must be >= 1.")
|
||||
raise typer.Exit(1)
|
||||
if not (0.0 <= threshold <= 1.0):
|
||||
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Searching memories...") as _ts:
|
||||
try:
|
||||
results = backend.search(
|
||||
query,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
top_k=top_k,
|
||||
threshold=threshold,
|
||||
rerank=rerank,
|
||||
keyword=keyword,
|
||||
filters=filters,
|
||||
fields=field_list,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
if output == "agent":
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="search",
|
||||
data=results,
|
||||
scope=scope or None,
|
||||
count=len(results),
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
elif output == "table":
|
||||
if results:
|
||||
format_memories_table(console, results, show_score=True)
|
||||
print_result_summary(
|
||||
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||
)
|
||||
else:
|
||||
console.print()
|
||||
print_info(console, "No memories found matching your query.")
|
||||
console.print()
|
||||
else:
|
||||
if results:
|
||||
format_memories_text(console, results)
|
||||
print_result_summary(
|
||||
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||
)
|
||||
else:
|
||||
console.print()
|
||||
print_info(console, "No memories found matching your query.")
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
||||
"""Get a specific memory by ID."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("get")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
with timed_status(err_console, "Fetching memory...") as _ts:
|
||||
try:
|
||||
result = backend.get(memory_id)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(console, command="get", data=result)
|
||||
else:
|
||||
format_single_memory(console, result, output)
|
||||
|
||||
|
||||
def cmd_list(
|
||||
backend: Backend,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
page: int,
|
||||
page_size: int,
|
||||
category: str | None,
|
||||
after: str | None,
|
||||
before: str | None,
|
||||
output: str = "table",
|
||||
) -> None:
|
||||
"""List memories."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("list")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
if page_size < 1:
|
||||
print_error(err_console, "--page-size must be >= 1.")
|
||||
raise typer.Exit(1)
|
||||
if page < 1:
|
||||
print_error(err_console, "--page must be >= 1.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Listing memories...") as _ts:
|
||||
try:
|
||||
results = backend.list_memories(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
category=category,
|
||||
after=after,
|
||||
before=before,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
if output in ("json", "agent"):
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="list",
|
||||
data=results,
|
||||
scope=scope or None,
|
||||
count=len(results),
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "table":
|
||||
if results:
|
||||
format_memories_table(console, results)
|
||||
print_result_summary(
|
||||
console,
|
||||
len(results),
|
||||
duration_secs=_elapsed,
|
||||
page=page,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
else:
|
||||
console.print()
|
||||
print_info(console, "No memories found.")
|
||||
console.print()
|
||||
else:
|
||||
if results:
|
||||
format_memories_text(console, results, title="memories")
|
||||
print_result_summary(
|
||||
console,
|
||||
len(results),
|
||||
duration_secs=_elapsed,
|
||||
page=page,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
else:
|
||||
console.print()
|
||||
print_info(console, "No memories found.")
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_update(
|
||||
backend: Backend,
|
||||
memory_id: str,
|
||||
text: str | None,
|
||||
*,
|
||||
metadata: str | None,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Update a memory."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("update")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
meta = None
|
||||
if metadata:
|
||||
try:
|
||||
meta = json.loads(metadata)
|
||||
except json.JSONDecodeError:
|
||||
print_error(err_console, "Invalid JSON in --metadata.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Updating memory...") as _ts:
|
||||
try:
|
||||
result = backend.update(memory_id, content=text, metadata=meta)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="update",
|
||||
data=result,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
|
||||
|
||||
|
||||
def cmd_delete(
|
||||
backend: Backend,
|
||||
memory_id: str,
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
force: bool = False,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete a single memory by ID."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("delete")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
if dry_run:
|
||||
# Fetch and display what would be deleted
|
||||
try:
|
||||
mem = backend.get(memory_id)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
format_single_memory(console, mem, output)
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting...") as _ts:
|
||||
try:
|
||||
result = backend.delete(memory_id=memory_id)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="delete",
|
||||
data={"id": memory_id, "deleted": True},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
|
||||
|
||||
|
||||
def cmd_delete_all(
|
||||
backend: Backend,
|
||||
*,
|
||||
force: bool,
|
||||
dry_run: bool = False,
|
||||
all_: bool = False,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
app_id: str | None,
|
||||
run_id: str | None,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete all memories matching a scope."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("delete-all")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
if not force:
|
||||
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||
raise typer.Exit(1)
|
||||
if all_:
|
||||
# Project-wide wipe using wildcard entity IDs
|
||||
# Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||
|
||||
if not force:
|
||||
confirm = typer.confirm(
|
||||
"\n ⚠ Delete ALL memories across the ENTIRE project? This cannot be undone."
|
||||
)
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled.")
|
||||
raise typer.Exit(0)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting all memories project-wide...") as _ts:
|
||||
try:
|
||||
result = backend.delete(
|
||||
all=True,
|
||||
user_id="*",
|
||||
agent_id="*",
|
||||
app_id="*",
|
||||
run_id="*",
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="delete-all",
|
||||
data={"deleted": True, "scope": "project"},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
if isinstance(result, dict) and "message" in result:
|
||||
print_info(console, "Deletion started. Memories will be removed in the background.")
|
||||
else:
|
||||
print_success(console, f"All project memories deleted ({_elapsed:.2f}s)")
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
# List matching memories and show count
|
||||
try:
|
||||
results = backend.list_memories(
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
count = len(results)
|
||||
print_info(console, f"Would delete {count} memor{'y' if count == 1 else 'ies'}.")
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
if not force:
|
||||
scope_parts = []
|
||||
if user_id:
|
||||
scope_parts.append(f"user={user_id}")
|
||||
if agent_id:
|
||||
scope_parts.append(f"agent={agent_id}")
|
||||
if app_id:
|
||||
scope_parts.append(f"app={app_id}")
|
||||
if run_id:
|
||||
scope_parts.append(f"run={run_id}")
|
||||
scope = ", ".join(scope_parts) if scope_parts else "ALL entities"
|
||||
|
||||
confirm = typer.confirm(f"\n ⚠ Delete ALL memories for {scope}? This cannot be undone.")
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled.")
|
||||
raise typer.Exit(0)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting all memories...") as _ts:
|
||||
try:
|
||||
result = backend.delete(
|
||||
all=True,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
app_id=app_id,
|
||||
run_id=run_id,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="delete-all",
|
||||
data={"deleted": True},
|
||||
scope=scope or None,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
if isinstance(result, dict) and "message" in result:
|
||||
print_info(console, "Deletion started. Memories will be removed in the background.")
|
||||
else:
|
||||
print_success(console, f"All matching memories deleted ({_elapsed:.2f}s)")
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Utility commands: status, version, import."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time as _time
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import track
|
||||
|
||||
from mem0_cli import __version__
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.branding import (
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
ERROR_COLOR,
|
||||
SUCCESS_COLOR,
|
||||
print_error,
|
||||
print_success,
|
||||
timed_status,
|
||||
)
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def cmd_status(
|
||||
backend: Backend,
|
||||
*,
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Check connectivity and auth."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("status")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Checking connection...") as _ts:
|
||||
result = backend.status(user_id=user_id, agent_id=agent_id)
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output in ("json", "agent"):
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="status",
|
||||
data={
|
||||
"connected": result.get("connected", False),
|
||||
"backend": result.get("backend", "?"),
|
||||
"base_url": result.get("base_url", ""),
|
||||
},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
lines = []
|
||||
if result.get("connected"):
|
||||
lines.append(f" [{SUCCESS_COLOR}]●[/] Connected")
|
||||
else:
|
||||
lines.append(f" [{ERROR_COLOR}]●[/] Disconnected")
|
||||
|
||||
lines.append(f" [{DIM_COLOR}]Backend:[/] {result.get('backend', '?')}")
|
||||
if result.get("base_url"):
|
||||
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
|
||||
if result.get("error"):
|
||||
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
|
||||
if "Authentication failed" in str(result["error"]):
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key[/]"
|
||||
)
|
||||
lines.append(
|
||||
f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/bold][/]"
|
||||
)
|
||||
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
|
||||
|
||||
content = "\n".join(lines)
|
||||
panel = Panel(
|
||||
content,
|
||||
title=f"[{BRAND_COLOR}]Connection Status[/]",
|
||||
title_align="left",
|
||||
border_style=BRAND_COLOR,
|
||||
padding=(1, 1),
|
||||
)
|
||||
console.print()
|
||||
console.print(panel)
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_version() -> None:
|
||||
"""Show version."""
|
||||
console.print(f" [{BRAND_COLOR}]◆ Mem0[/] CLI v{__version__}")
|
||||
|
||||
|
||||
def cmd_import(
|
||||
backend: Backend,
|
||||
file_path: str,
|
||||
*,
|
||||
user_id: str | None,
|
||||
agent_id: str | None,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Import memories from a JSON file."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("import")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
|
||||
try:
|
||||
data = json.loads(Path(file_path).read_text())
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
print_error(err_console, f"Failed to read file: {e}")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if not isinstance(data, list):
|
||||
data = [data]
|
||||
|
||||
added = 0
|
||||
failed = 0
|
||||
_start = _time.perf_counter()
|
||||
for item in track(
|
||||
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
|
||||
):
|
||||
content = item.get("memory", item.get("text", item.get("content", "")))
|
||||
if not content:
|
||||
failed += 1
|
||||
continue
|
||||
try:
|
||||
backend.add(
|
||||
content=content,
|
||||
user_id=user_id or item.get("user_id"),
|
||||
agent_id=agent_id or item.get("agent_id"),
|
||||
metadata=item.get("metadata"),
|
||||
)
|
||||
added += 1
|
||||
except Exception:
|
||||
failed += 1
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output in ("json", "agent"):
|
||||
scope = {k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="import",
|
||||
data={"added": added, "failed": failed},
|
||||
scope=scope or None,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
print_success(err_console, f"Imported {added} memories ({_elapsed:.2f}s)")
|
||||
if failed:
|
||||
print_error(err_console, f"{failed} memories failed to import.")
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Configuration management for mem0 CLI.
|
||||
|
||||
Config precedence (highest to lowest):
|
||||
1. CLI flags (--api-key, --base-url, etc.)
|
||||
2. Environment variables (MEM0_API_KEY, etc.)
|
||||
3. Config file (~/.mem0/config.json)
|
||||
4. Defaults
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
CONFIG_DIR = Path.home() / ".mem0"
|
||||
CONFIG_FILE = CONFIG_DIR / "config.json"
|
||||
|
||||
DEFAULT_BASE_URL = "https://api.mem0.ai"
|
||||
CONFIG_VERSION = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformConfig:
|
||||
api_key: str = ""
|
||||
base_url: str = DEFAULT_BASE_URL
|
||||
user_email: str = ""
|
||||
# Agent Mode (unclaimed-shadow signup)
|
||||
agent_mode: bool = False # True while the key is an unclaimed agent-mode key
|
||||
created_via: str = "" # "agent_mode" | "email" | "api_key" | "existing_key"
|
||||
agent_caller: str = (
|
||||
"" # canonical agent name when created_via == "agent_mode" (e.g. "claude-code")
|
||||
)
|
||||
claimed_at: str = "" # ISO timestamp once the agent has been claimed by a human
|
||||
default_user_id: str = "" # `user_<slug>` returned by bootstrap; used as auto-default
|
||||
|
||||
|
||||
@dataclass
|
||||
class DefaultsConfig:
|
||||
user_id: str = ""
|
||||
agent_id: str = ""
|
||||
app_id: str = ""
|
||||
run_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TelemetryConfig:
|
||||
anonymous_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mem0Config:
|
||||
version: int = CONFIG_VERSION
|
||||
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
||||
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
||||
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
|
||||
|
||||
|
||||
SHORT_KEY_ALIASES: dict[str, str] = {
|
||||
"api_key": "platform.api_key",
|
||||
"base_url": "platform.base_url",
|
||||
"user_email": "platform.user_email",
|
||||
"user_id": "defaults.user_id",
|
||||
"agent_id": "defaults.agent_id",
|
||||
"app_id": "defaults.app_id",
|
||||
"run_id": "defaults.run_id",
|
||||
}
|
||||
|
||||
|
||||
def ensure_config_dir() -> Path:
|
||||
"""Create ~/.mem0 directory with secure permissions if it doesn't exist."""
|
||||
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(CONFIG_DIR, stat.S_IRWXU) # 0700
|
||||
return CONFIG_DIR
|
||||
|
||||
|
||||
def load_config() -> Mem0Config:
|
||||
"""Load config from file, applying env var overrides."""
|
||||
config = Mem0Config()
|
||||
|
||||
if CONFIG_FILE.exists():
|
||||
with open(CONFIG_FILE) as f:
|
||||
data = json.load(f)
|
||||
|
||||
config.version = data.get("version", CONFIG_VERSION)
|
||||
|
||||
plat = data.get("platform", {})
|
||||
config.platform.api_key = plat.get("api_key", "")
|
||||
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||
config.platform.user_email = plat.get("user_email", "")
|
||||
config.platform.agent_mode = bool(plat.get("agent_mode", False))
|
||||
config.platform.created_via = plat.get("created_via", "")
|
||||
config.platform.agent_caller = plat.get("agent_caller", "")
|
||||
config.platform.claimed_at = plat.get("claimed_at", "")
|
||||
config.platform.default_user_id = plat.get("default_user_id", "")
|
||||
|
||||
defaults = data.get("defaults", {})
|
||||
config.defaults.user_id = defaults.get("user_id", "")
|
||||
config.defaults.agent_id = defaults.get("agent_id", "")
|
||||
config.defaults.app_id = defaults.get("app_id", "")
|
||||
config.defaults.run_id = defaults.get("run_id", "")
|
||||
telemetry = data.get("telemetry", {})
|
||||
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
|
||||
|
||||
# Environment variable overrides
|
||||
env_key = os.environ.get("MEM0_API_KEY")
|
||||
if env_key:
|
||||
config.platform.api_key = env_key
|
||||
|
||||
env_base = os.environ.get("MEM0_BASE_URL")
|
||||
if env_base:
|
||||
config.platform.base_url = env_base
|
||||
|
||||
env_user_id = os.environ.get("MEM0_USER_ID")
|
||||
if env_user_id:
|
||||
config.defaults.user_id = env_user_id
|
||||
|
||||
env_agent_id = os.environ.get("MEM0_AGENT_ID")
|
||||
if env_agent_id:
|
||||
config.defaults.agent_id = env_agent_id
|
||||
|
||||
env_app_id = os.environ.get("MEM0_APP_ID")
|
||||
if env_app_id:
|
||||
config.defaults.app_id = env_app_id
|
||||
|
||||
env_run_id = os.environ.get("MEM0_RUN_ID")
|
||||
if env_run_id:
|
||||
config.defaults.run_id = env_run_id
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def save_config(config: Mem0Config) -> None:
|
||||
"""Write config to disk with secure permissions."""
|
||||
ensure_config_dir()
|
||||
|
||||
data: dict[str, Any] = {
|
||||
"version": config.version,
|
||||
"defaults": {
|
||||
"user_id": config.defaults.user_id,
|
||||
"agent_id": config.defaults.agent_id,
|
||||
"app_id": config.defaults.app_id,
|
||||
"run_id": config.defaults.run_id,
|
||||
},
|
||||
"platform": {
|
||||
"api_key": config.platform.api_key,
|
||||
"base_url": config.platform.base_url,
|
||||
"user_email": config.platform.user_email,
|
||||
"agent_mode": config.platform.agent_mode,
|
||||
"created_via": config.platform.created_via,
|
||||
"agent_caller": config.platform.agent_caller,
|
||||
"claimed_at": config.platform.claimed_at,
|
||||
"default_user_id": config.platform.default_user_id,
|
||||
},
|
||||
"telemetry": {
|
||||
"anonymous_id": config.telemetry.anonymous_id,
|
||||
},
|
||||
}
|
||||
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
||||
|
||||
# Propagate the active api_key to ecosystem touchpoints (Claude Code
|
||||
# plugin env injection, shell rc exports). Idempotent — only updates
|
||||
# EXISTING entries; never creates new ones. Best-effort: any IOError
|
||||
# in the sync is swallowed so config.json is always the authoritative
|
||||
# write, never blocked by plugin-state issues.
|
||||
if config.platform.api_key:
|
||||
try:
|
||||
from mem0_cli.plugin_sync import sync_api_key
|
||||
|
||||
sync_api_key(config.platform.api_key)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def redact_key(key: str) -> str:
|
||||
"""Redact an API key for display: m0-xxx...xxx"""
|
||||
if not key:
|
||||
return "(not set)"
|
||||
if len(key) <= 8:
|
||||
return key[:2] + "***"
|
||||
return key[:4] + "..." + key[-4:]
|
||||
|
||||
|
||||
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
||||
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'."""
|
||||
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
|
||||
parts = dotted_key.split(".")
|
||||
obj: Any = config
|
||||
for part in parts:
|
||||
if hasattr(obj, part):
|
||||
obj = getattr(obj, part)
|
||||
else:
|
||||
return None
|
||||
return obj
|
||||
|
||||
|
||||
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
||||
"""Set a config value by dotted path. Returns True on success."""
|
||||
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
|
||||
parts = dotted_key.split(".")
|
||||
obj: Any = config
|
||||
for part in parts[:-1]:
|
||||
if hasattr(obj, part):
|
||||
obj = getattr(obj, part)
|
||||
else:
|
||||
return False
|
||||
|
||||
final_key = parts[-1]
|
||||
if not hasattr(obj, final_key):
|
||||
return False
|
||||
|
||||
current = getattr(obj, final_key)
|
||||
# Type coercion
|
||||
if isinstance(current, bool):
|
||||
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
|
||||
elif isinstance(current, int):
|
||||
value = int(value) # type: ignore[assignment]
|
||||
|
||||
setattr(obj, final_key, value)
|
||||
return True
|
||||
@@ -0,0 +1,378 @@
|
||||
"""Output formatting for mem0 CLI — text, JSON, table, quiet modes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, SUCCESS_COLOR, _sym
|
||||
|
||||
|
||||
def format_memories_text(console: Console, memories: list[dict], title: str = "memories") -> None:
|
||||
"""Render memories in human-friendly text mode."""
|
||||
count = len(memories)
|
||||
console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
|
||||
|
||||
for i, mem in enumerate(memories, 1):
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
mem_id = mem.get("id", "")[:8]
|
||||
score = mem.get("score")
|
||||
created = _format_date(mem.get("created_at"))
|
||||
category = mem.get("categories", [None])
|
||||
if isinstance(category, list):
|
||||
category = category[0] if category else None
|
||||
|
||||
line = Text()
|
||||
line.append(f" {i}. ", style="bold")
|
||||
line.append(memory_text, style="white")
|
||||
console.print(line)
|
||||
|
||||
details = []
|
||||
if score is not None:
|
||||
details.append(f"Score: {score:.2f}")
|
||||
if mem_id:
|
||||
details.append(f"ID: {mem_id}")
|
||||
if created:
|
||||
details.append(f"Created: {created}")
|
||||
if category:
|
||||
details.append(f"Category: {category}")
|
||||
|
||||
if details:
|
||||
detail_str = " · ".join(details)
|
||||
console.print(f" [{DIM_COLOR}]{detail_str}[/]")
|
||||
console.print()
|
||||
|
||||
|
||||
def format_memories_table(
|
||||
console: Console, memories: list[dict], *, show_score: bool = False
|
||||
) -> None:
|
||||
"""Render memories in a rich table."""
|
||||
table = Table(
|
||||
border_style=BRAND_COLOR,
|
||||
header_style=f"bold {ACCENT_COLOR}",
|
||||
row_styles=["", "dim"],
|
||||
padding=(0, 1),
|
||||
)
|
||||
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
|
||||
if show_score:
|
||||
table.add_column("Score", max_width=7, justify="right")
|
||||
table.add_column("Memory", max_width=50, no_wrap=False)
|
||||
table.add_column("Category", max_width=14)
|
||||
table.add_column("Created", max_width=12)
|
||||
|
||||
for mem in memories:
|
||||
mem_id = mem.get("id", "")
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
if len(memory_text) > 60:
|
||||
memory_text = memory_text[:57] + "..."
|
||||
categories = mem.get("categories", [])
|
||||
if isinstance(categories, list) and categories:
|
||||
cat = (
|
||||
categories[0]
|
||||
if len(categories) == 1
|
||||
else f"{categories[0]} (+{len(categories) - 1})"
|
||||
)
|
||||
else:
|
||||
cat = "—"
|
||||
created = _format_date(mem.get("created_at")) or "—"
|
||||
if show_score:
|
||||
score = mem.get("score")
|
||||
score_str = f"{score:.2f}" if score is not None else "—"
|
||||
table.add_row(mem_id, score_str, memory_text, cat, created)
|
||||
else:
|
||||
table.add_row(mem_id, memory_text, cat, created)
|
||||
|
||||
console.print()
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
|
||||
def format_json(console: Console, data: Any) -> None:
|
||||
"""Output data as pretty-printed JSON."""
|
||||
console.print_json(json.dumps(data, default=str))
|
||||
|
||||
|
||||
def format_single_memory(console: Console, mem: dict, output: str = "text") -> None:
|
||||
"""Format a single memory for display."""
|
||||
if output == "json":
|
||||
format_json(console, mem)
|
||||
return
|
||||
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
mem_id = mem.get("id", "")
|
||||
|
||||
lines = []
|
||||
lines.append(f" [white bold]{memory_text}[/]")
|
||||
lines.append("")
|
||||
|
||||
if mem_id:
|
||||
lines.append(f" [{DIM_COLOR}]ID:[/] {mem_id}")
|
||||
created = _format_date(mem.get("created_at"))
|
||||
if created:
|
||||
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
|
||||
updated = _format_date(mem.get("updated_at"))
|
||||
if updated:
|
||||
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
|
||||
meta = mem.get("metadata")
|
||||
if meta:
|
||||
lines.append(f" [{DIM_COLOR}]Metadata:[/] {json.dumps(meta)}")
|
||||
categories = mem.get("categories")
|
||||
if categories:
|
||||
cat_str = ", ".join(categories) if isinstance(categories, list) else categories
|
||||
lines.append(f" [{DIM_COLOR}]Categories:[/] {cat_str}")
|
||||
|
||||
content = "\n".join(lines)
|
||||
panel = Panel(
|
||||
content,
|
||||
title=f"[{BRAND_COLOR}]Memory[/]",
|
||||
title_align="left",
|
||||
border_style=BRAND_COLOR,
|
||||
padding=(1, 1),
|
||||
)
|
||||
console.print()
|
||||
console.print(panel)
|
||||
console.print()
|
||||
|
||||
|
||||
def format_add_result(console: Console, result: dict | list, output: str = "text") -> None:
|
||||
"""Format the result of an add operation."""
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
return
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
# result from API is typically {"results": [...]}
|
||||
results = result if isinstance(result, list) else result.get("results", [result])
|
||||
if not results:
|
||||
console.print(f" [{DIM_COLOR}]No memories extracted.[/]")
|
||||
return
|
||||
|
||||
console.print()
|
||||
seen_pending_events: set[str] = set()
|
||||
for r in results:
|
||||
# Detect async PENDING response from Platform API
|
||||
if r.get("status") == "PENDING":
|
||||
event_id = r.get("event_id", "")
|
||||
# Deduplicate PENDING entries with the same event_id
|
||||
if event_id and event_id in seen_pending_events:
|
||||
continue
|
||||
if event_id:
|
||||
seen_pending_events.add(event_id)
|
||||
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
|
||||
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
|
||||
parts.append("[white]Processing in background[/]")
|
||||
console.print(" ".join(parts))
|
||||
if event_id:
|
||||
console.print(f" [{DIM_COLOR}] event_id: {event_id}[/]")
|
||||
console.print(f" [{DIM_COLOR}] → Check status: mem0 event status {event_id}[/]")
|
||||
continue
|
||||
|
||||
event = r.get("event", "ADD")
|
||||
memory = r.get("memory") or r.get("text") or r.get("content") or r.get("data") or ""
|
||||
mem_id = (r.get("id") or r.get("memory_id") or "")[:8]
|
||||
|
||||
if event == "ADD":
|
||||
icon = f"[{SUCCESS_COLOR}]+[/]"
|
||||
label = "Added"
|
||||
elif event == "UPDATE":
|
||||
icon = f"[{ACCENT_COLOR}]~[/]"
|
||||
label = "Updated"
|
||||
elif event == "DELETE":
|
||||
icon = "[red]-[/]"
|
||||
label = "Deleted"
|
||||
elif event == "NOOP":
|
||||
icon = f"[{DIM_COLOR}]·[/]"
|
||||
label = "No change"
|
||||
else:
|
||||
icon = f"[{DIM_COLOR}]?[/]"
|
||||
label = event
|
||||
|
||||
# Build the display line
|
||||
parts = [f" {icon} [{DIM_COLOR}]{label:<10}[/]"]
|
||||
if memory:
|
||||
parts.append(f"[white]{memory}[/]")
|
||||
if mem_id:
|
||||
parts.append(f"[{DIM_COLOR}]({mem_id})[/]")
|
||||
console.print(" ".join(parts))
|
||||
console.print()
|
||||
|
||||
|
||||
def format_json_envelope(
|
||||
console: Console,
|
||||
*,
|
||||
command: str,
|
||||
data: Any,
|
||||
duration_ms: int | None = None,
|
||||
scope: dict | None = None,
|
||||
count: int | None = None,
|
||||
status: str = "success",
|
||||
error: str | None = None,
|
||||
) -> None:
|
||||
"""Output structured JSON envelope for AI agent consumption."""
|
||||
envelope: dict[str, Any] = {
|
||||
"status": status,
|
||||
"command": command,
|
||||
}
|
||||
if duration_ms is not None:
|
||||
envelope["duration_ms"] = duration_ms
|
||||
if scope is not None:
|
||||
envelope["scope"] = scope
|
||||
if count is not None:
|
||||
envelope["count"] = count
|
||||
if error:
|
||||
envelope["error"] = error
|
||||
envelope["data"] = data
|
||||
|
||||
# If the platform flagged this as an unclaimed Agent Mode account, surface
|
||||
# the notice inside the JSON envelope so an agent consuming the output
|
||||
# sees it without needing to inspect HTTP headers.
|
||||
from mem0_cli.state import take_notice
|
||||
|
||||
notice = take_notice()
|
||||
if notice:
|
||||
envelope["mem0_notice"] = notice
|
||||
|
||||
console.print_json(json.dumps(envelope, default=str))
|
||||
|
||||
|
||||
def sanitize_agent_data(command: str, data: Any) -> Any:
|
||||
"""Project API response data to minimal relevant fields for agent consumption."""
|
||||
|
||||
def pick(obj: dict, keys: list) -> dict:
|
||||
return {k: obj[k] for k in keys if k in obj}
|
||||
|
||||
if data is None:
|
||||
return data
|
||||
|
||||
if command == "add":
|
||||
items = data if isinstance(data, list) else [data]
|
||||
result = []
|
||||
for item in items:
|
||||
if item.get("status") == "PENDING":
|
||||
result.append(pick(item, ["status", "event_id"]))
|
||||
else:
|
||||
result.append(pick(item, ["id", "memory", "event"]))
|
||||
return result
|
||||
|
||||
if command == "search":
|
||||
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
|
||||
|
||||
if command == "list":
|
||||
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
|
||||
|
||||
if command == "get":
|
||||
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
|
||||
|
||||
if command == "update":
|
||||
return pick(data, ["id", "memory"])
|
||||
|
||||
if command in ("delete", "delete-all", "entity delete"):
|
||||
return data
|
||||
|
||||
if command == "entity list":
|
||||
result = []
|
||||
for r in data:
|
||||
item = pick(r, ["type", "count"])
|
||||
item["name"] = r.get("name") or r.get("id", "")
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
if command == "event list":
|
||||
return [pick(r, ["id", "event_type", "status", "latency", "created_at"]) for r in data]
|
||||
|
||||
if command == "event status":
|
||||
ev = data
|
||||
raw_results = ev.get("results") or []
|
||||
sanitized_results = []
|
||||
for r in raw_results:
|
||||
nested = r.get("data") or {}
|
||||
memory = nested.get("memory") if isinstance(nested, dict) else None
|
||||
sanitized_results.append(
|
||||
{
|
||||
"id": r.get("id"),
|
||||
"event": r.get("event"),
|
||||
"user_id": r.get("user_id"),
|
||||
"memory": memory,
|
||||
}
|
||||
)
|
||||
result = pick(ev, ["id", "event_type", "status", "latency", "created_at", "updated_at"])
|
||||
result["results"] = sanitized_results
|
||||
return result
|
||||
|
||||
# Pass-through: status, import, config show/get/set
|
||||
return data
|
||||
|
||||
|
||||
def format_agent_envelope(
|
||||
console: Console,
|
||||
*,
|
||||
command: str,
|
||||
data: Any,
|
||||
duration_ms: int | None = None,
|
||||
scope: dict | None = None,
|
||||
count: int | None = None,
|
||||
) -> None:
|
||||
"""Output structured JSON envelope for agent/programmatic use (--json/--agent mode)."""
|
||||
envelope: dict[str, Any] = {
|
||||
"status": "success",
|
||||
"command": command,
|
||||
}
|
||||
if duration_ms is not None:
|
||||
envelope["duration_ms"] = duration_ms
|
||||
if scope:
|
||||
filtered = {k: v for k, v in scope.items() if v}
|
||||
if filtered:
|
||||
envelope["scope"] = filtered
|
||||
if count is not None:
|
||||
envelope["count"] = count
|
||||
envelope["data"] = sanitize_agent_data(command, data)
|
||||
|
||||
# Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
||||
# agent reading the JSON output sees it without inspecting HTTP headers.
|
||||
from mem0_cli.state import take_notice
|
||||
|
||||
notice = take_notice()
|
||||
if notice:
|
||||
envelope["mem0_notice"] = notice
|
||||
|
||||
console.print_json(json.dumps(envelope, default=str))
|
||||
|
||||
|
||||
def print_result_summary(
|
||||
console: Console,
|
||||
count: int,
|
||||
*,
|
||||
duration_secs: float | None = None,
|
||||
page: int | None = None,
|
||||
**scope_ids: str | None,
|
||||
) -> None:
|
||||
"""Print a summary footer after result lists."""
|
||||
parts = [f"{count} result{'s' if count != 1 else ''}"]
|
||||
if page is not None:
|
||||
parts.append(f"page {page}")
|
||||
scope_parts = [f"{k}={v}" for k, v in scope_ids.items() if v]
|
||||
if scope_parts:
|
||||
parts.append(", ".join(scope_parts))
|
||||
if duration_secs is not None:
|
||||
parts.append(f"{duration_secs:.2f}s")
|
||||
|
||||
summary = " · ".join(parts)
|
||||
console.print(f" [{DIM_COLOR}]{summary}[/]")
|
||||
console.print()
|
||||
|
||||
|
||||
def _format_date(dt_str: str | None) -> str | None:
|
||||
if not dt_str:
|
||||
return None
|
||||
try:
|
||||
dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
except (ValueError, AttributeError):
|
||||
return str(dt_str)[:10] if dt_str else None
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Sync the active Mem0 API key into other ecosystem touchpoints.
|
||||
|
||||
Why this exists:
|
||||
The CLI canonical state lives in ``~/.mem0/config.json``. But MCP servers
|
||||
(Claude Code plugin, Codex plugin, etc.) read ``MEM0_API_KEY`` from env
|
||||
vars or their own config files. Without a sync, an agent-mode bootstrap
|
||||
mints a new key into config.json but the plugin's MCP keeps using the
|
||||
old key from env — silent surprise.
|
||||
|
||||
Design:
|
||||
- Update ONLY entries that already exist (never create new ones)
|
||||
- Preserve all surrounding content / formatting / other keys
|
||||
- Atomic writes (tmpfile + rename) so a crash mid-write doesn't corrupt
|
||||
- Idempotent — re-running with the same key is a no-op
|
||||
- Skip on dry_run
|
||||
|
||||
Targets currently handled:
|
||||
- ``~/.claude/settings.json::env::MEM0_API_KEY`` (Claude Code env injection)
|
||||
- ``~/.zshrc`` / ``~/.bashrc`` ``export MEM0_API_KEY="..."`` lines
|
||||
|
||||
Out of scope (deliberately not touched):
|
||||
- Codex / Cursor MCP configs — would require schema-aware edits and
|
||||
those tools don't have mem0 entries by default
|
||||
- Plugin's own ``<plugin-dir>/.api_key`` file — plugin-managed
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Files we know how to update safely.
|
||||
_CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
|
||||
_SHELL_RCS = [Path.home() / ".zshrc", Path.home() / ".bashrc", Path.home() / ".bash_profile"]
|
||||
|
||||
|
||||
def sync_api_key(api_key: str) -> list[str]:
|
||||
"""Propagate ``api_key`` into known ecosystem touchpoints.
|
||||
|
||||
Returns the list of paths actually updated. Empty list means nothing
|
||||
needed updating (either targets didn't exist or already had this value).
|
||||
"""
|
||||
if not api_key:
|
||||
return []
|
||||
updated: list[str] = []
|
||||
if _update_claude_settings(_CLAUDE_SETTINGS, api_key):
|
||||
updated.append(str(_CLAUDE_SETTINGS))
|
||||
for rc in _SHELL_RCS:
|
||||
if _update_shell_rc(rc, api_key):
|
||||
updated.append(str(rc))
|
||||
return updated
|
||||
|
||||
|
||||
def _update_claude_settings(path: Path, api_key: str) -> bool:
|
||||
"""Update ``env.MEM0_API_KEY`` in path. Returns True if file was changed."""
|
||||
if not path.is_file():
|
||||
return False
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return False
|
||||
env = data.get("env")
|
||||
if not isinstance(env, dict) or "MEM0_API_KEY" not in env:
|
||||
# No existing entry — don't create one.
|
||||
return False
|
||||
if env["MEM0_API_KEY"] == api_key:
|
||||
return False # already in sync
|
||||
env["MEM0_API_KEY"] = api_key
|
||||
_atomic_write_text(path, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
||||
return True
|
||||
|
||||
|
||||
# Match `export MEM0_API_KEY="..."` (or single quotes, or no quotes).
|
||||
# Use [ \t]* (not \s*) for trailing whitespace so a trailing newline at
|
||||
# end-of-file is preserved when MEM0_API_KEY is the last line.
|
||||
_RC_LINE = re.compile(
|
||||
r'^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["\']?)([^"\'\n]*)(["\']?)[ \t]*$',
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def _update_shell_rc(path: Path, api_key: str) -> bool:
|
||||
"""Update an existing ``export MEM0_API_KEY=...`` line in path."""
|
||||
if not path.is_file():
|
||||
return False
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return False
|
||||
match = _RC_LINE.search(text)
|
||||
if not match:
|
||||
return False # no existing line
|
||||
if match.group(3) == api_key:
|
||||
return False
|
||||
new_text = _RC_LINE.sub(lambda m: f'{m.group(1)}"{api_key}"', text, count=1)
|
||||
_atomic_write_text(path, new_text)
|
||||
return True
|
||||
|
||||
|
||||
def _atomic_write_text(path: Path, content: str) -> None:
|
||||
"""Write content to path atomically (temp + rename)."""
|
||||
dirname = path.parent
|
||||
fd, tmp_path = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=dirname)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
# Preserve mode if the original existed.
|
||||
if path.exists():
|
||||
os.chmod(tmp_path, path.stat().st_mode & 0o777)
|
||||
os.replace(tmp_path, path)
|
||||
except Exception:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(tmp_path)
|
||||
raise
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Agent mode state — set by the root callback, read by commands and branding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
_agent_mode: bool = False
|
||||
_current_command: str = ""
|
||||
_pending_notice: str = ""
|
||||
|
||||
|
||||
def is_agent_mode() -> bool:
|
||||
return _agent_mode
|
||||
|
||||
|
||||
def set_agent_mode(val: bool) -> None:
|
||||
global _agent_mode
|
||||
_agent_mode = val
|
||||
|
||||
|
||||
def get_current_command() -> str:
|
||||
return _current_command
|
||||
|
||||
|
||||
def set_current_command(name: str) -> None:
|
||||
global _current_command
|
||||
_current_command = name
|
||||
|
||||
|
||||
def capture_notice(notice: str | None) -> None:
|
||||
"""Stash a Mem0 backend notice for end-of-command surfacing.
|
||||
|
||||
Called from the platform backend after each response so the notice can
|
||||
be printed once per command (regardless of how many sub-requests fired).
|
||||
Last-write-wins is fine — the message text is identical across requests.
|
||||
"""
|
||||
global _pending_notice
|
||||
if notice:
|
||||
_pending_notice = notice
|
||||
|
||||
|
||||
def take_notice() -> str:
|
||||
"""Return and clear the pending notice."""
|
||||
global _pending_notice
|
||||
msg = _pending_notice
|
||||
_pending_notice = ""
|
||||
return msg
|
||||
@@ -0,0 +1,148 @@
|
||||
"""CLI telemetry — anonymous usage tracking via PostHog.
|
||||
|
||||
Sends fire-and-forget events to PostHog by spawning a detached subprocess
|
||||
(telemetry_sender.py). The parent CLI process exits immediately; the
|
||||
subprocess handles email resolution, caching, and the HTTP POST.
|
||||
|
||||
Disable with: MEM0_TELEMETRY=false
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
|
||||
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
|
||||
|
||||
|
||||
def _is_telemetry_enabled() -> bool:
|
||||
val = os.environ.get("MEM0_TELEMETRY", "true").lower()
|
||||
return val not in ("false", "0", "no")
|
||||
|
||||
|
||||
def _get_or_create_anonymous_id() -> str:
|
||||
"""Return a persistent per-machine anonymous ID, generating one if needed.
|
||||
|
||||
Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
|
||||
repeat runs on the same machine share one PostHog identity instead of
|
||||
collapsing into a single shared fallback string.
|
||||
"""
|
||||
from mem0_cli.config import load_config, save_config
|
||||
|
||||
config = load_config()
|
||||
if config.telemetry.anonymous_id:
|
||||
return config.telemetry.anonymous_id
|
||||
|
||||
new_id = f"cli-anon-{uuid.uuid4().hex}"
|
||||
config.telemetry.anonymous_id = new_id
|
||||
with contextlib.suppress(Exception):
|
||||
save_config(config)
|
||||
return new_id
|
||||
|
||||
|
||||
def _get_distinct_id() -> str:
|
||||
"""Return a stable anonymous identifier for the current user.
|
||||
|
||||
Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
|
||||
persistent per-machine anonymous ID.
|
||||
"""
|
||||
try:
|
||||
from mem0_cli.config import load_config
|
||||
|
||||
config = load_config()
|
||||
if config.platform.user_email:
|
||||
return config.platform.user_email
|
||||
if config.platform.api_key:
|
||||
return hashlib.md5(config.platform.api_key.encode()).hexdigest()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
return _get_or_create_anonymous_id()
|
||||
except Exception:
|
||||
return f"cli-anon-{uuid.uuid4().hex}"
|
||||
|
||||
|
||||
def capture_event(
|
||||
event_name: str,
|
||||
properties: dict[str, Any] | None = None,
|
||||
pre_resolved_email: str | None = None,
|
||||
) -> None:
|
||||
"""Fire a PostHog event via a detached subprocess (non-blocking).
|
||||
|
||||
When *pre_resolved_email* is provided (e.g. from an upfront ping
|
||||
validation), it is used directly as the PostHog distinct ID and the
|
||||
subprocess skips its own ``/v1/ping/`` call.
|
||||
"""
|
||||
if not _is_telemetry_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
from mem0_cli import __version__
|
||||
from mem0_cli.config import CONFIG_FILE, load_config, save_config
|
||||
|
||||
config = load_config()
|
||||
distinct_id = pre_resolved_email or _get_distinct_id()
|
||||
|
||||
# Detect anonymous → identified transition. If a stored anonymous_id
|
||||
# exists and we just resolved to a real identity, fire a one-shot
|
||||
# $identify event so PostHog stitches the pre-signup history onto
|
||||
# the authenticated profile. Clear the stored id so we don't re-alias.
|
||||
anon_id_to_alias: str | None = None
|
||||
if (
|
||||
distinct_id
|
||||
and not distinct_id.startswith("cli-anon-")
|
||||
and config.telemetry.anonymous_id
|
||||
):
|
||||
anon_id_to_alias = config.telemetry.anonymous_id
|
||||
config.telemetry.anonymous_id = ""
|
||||
with contextlib.suppress(Exception):
|
||||
save_config(config)
|
||||
|
||||
# M4: every cli.* event carries agent_mode based on the config flag
|
||||
# (unclaimed Agent Mode key). This is the growth-doc property used to
|
||||
# join init → add → search funnels in PostHog.
|
||||
payload = {
|
||||
"api_key": POSTHOG_API_KEY,
|
||||
"distinct_id": distinct_id,
|
||||
"event": event_name,
|
||||
"properties": {
|
||||
"source": "CLI",
|
||||
"language": "python",
|
||||
"cli_version": __version__,
|
||||
"agent_mode": bool(config.platform.agent_mode),
|
||||
"python_version": sys.version,
|
||||
"os": sys.platform,
|
||||
"os_version": platform.version(),
|
||||
"$process_person_profile": False,
|
||||
"$lib": "posthog-python",
|
||||
**(properties or {}),
|
||||
},
|
||||
}
|
||||
|
||||
context = {
|
||||
"payload": payload,
|
||||
"posthog_host": POSTHOG_HOST,
|
||||
"needs_email": not distinct_id or "@" not in distinct_id,
|
||||
"mem0_api_key": config.platform.api_key or "",
|
||||
"mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
|
||||
"config_path": str(CONFIG_FILE),
|
||||
"anon_distinct_id_to_alias": anon_id_to_alias,
|
||||
}
|
||||
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
close_fds=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user