Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,26 @@ memos get user_123 --format json --detail detail

- `[USER_ID]`:用户维度;实际上必填,但若不传 CLI 会回退到配置中的 `defaults.user_id`。
- `--user-id`:`[USER_ID]` 的兼容别名;可选;与 `[USER_ID]` 使用同样的回退规则。
- `--conversation-id`:会话维度;可选;默认取配置中的 `defaults.conversation_id`。传入与 `memos add` 相同的值可保证读路径与写路径作用域一致。
- `--page`:页码;可选;不传时接口默认值为 `1`。
- `--size`:指定每一类记忆在当前页返回的条目数量;可选;不传时接口默认值为 `10`。
- `--include-preference`:是否召回偏好记忆;可选;接受 `true` 或 `false`;不传时默认 `true`。
- `--include-tool-memory`:是否召回工具记忆;可选;接受 `true` 或 `false`;当前 CLI 已暴露该参数,但官方 `get_memory` 文档未说明不传时的接口默认值。
- `--format`:输出格式;可选;默认值为 `agent`。
- `--detail`:非 JSON 输出的详略级别;可选;默认值为 `simple`;支持 `simple`、`detail`。

### `memos list`

`memos get` 的别名,用于列出当前用户与会话作用域下的记忆。

示例:

```bash
memos list user_123 --conversation-id conv_001 --format table --detail simple
```

参数与 [`memos get`](#memos-get) 完全一致。

### `memos delete`

用于删除一条记忆,或删除某个用户的全部记忆。
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,26 @@ Parameters:

- `[USER_ID]`: Retrieval scope; effectively required, but if omitted the CLI falls back to configured `defaults.user_id`.
- `--user-id`: Alias of `[USER_ID]`; optional; same fallback as `[USER_ID]`.
- `--conversation-id`: Conversation scope for retrieval; optional; defaults to configured `defaults.conversation_id`. Passing the same value used by `memos add` keeps reads consistent with writes.
- `--page`: Page number; optional; API default is `1` when omitted.
- `--size`: Number of items returned per memory category on the current page; optional; API default is `10` when omitted.
- `--include-preference`: Whether to include preference memory; optional; accepts `true` or `false`; defaults to `true` when omitted.
- `--include-tool-memory`: Whether to include tool memory; optional; accepts `true` or `false`; current CLI exposes this flag, but the official `get_memory` docs do not state the API default when omitted.
- `--format`: Output format; optional; defaults to `agent`.
- `--detail`: Output detail level for non-JSON formats; optional; defaults to `simple`; supported values: `simple`, `detail`.

### `memos list`

Alias of `memos get`; lists memories scoped to the current user and conversation.

Example:

```bash
memos list user_123 --conversation-id conv_001 --format table --detail simple
```

Parameters are identical to [`memos get`](#memos-get).

### `memos delete`

Delete one memory, or delete all memories for a user, using the documented delete API.
Expand Down
3 changes: 2 additions & 1 deletion skills/memos-memory/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ Command examples:
- `memos extract "<message>" --user-id <USER_ID> --format json`
- `memos search "<query>" --user-id <USER_ID> --format agent --detail simple`
- `memos chat "<message>" --user-id <USER_ID> --format agent`
- `memos get <USER_ID> --format json --detail detail`
- `memos get <USER_ID> --conversation-id <CONVERSATION_ID> --format json --detail detail`
- `memos list <USER_ID> --conversation-id <CONVERSATION_ID> --format table --detail simple`
- `memos origin <MEMORY_ID> --format json`
- `memos delete <MEMORY_ID> --format json`
- `memos delete --user-id <USER_ID> --format json`
Expand Down
7 changes: 5 additions & 2 deletions skills/memos-memory/references/memos-get.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Common flags:

- `[USER_ID]`
- `--user-id`
- `--conversation-id`
- `--page`
- `--size`
- `--filter`
Expand All @@ -35,9 +36,11 @@ Common flags:
Example:

```bash
memos get user_123 --format json --detail detail
memos get user_123 --conversation-id conv_001 --format json --detail detail
```

Working rules:
- `get` returns scoped records for the requested `user_id`;
- `get` returns scoped records for the requested `user_id` and `conversation_id`;
- when `--conversation-id` is omitted, the CLI uses the configured `defaults.conversation_id`; pass the same conversation used by `memos add` so reads see the records just written;
- `memos list` is an alias of `memos get` and accepts the same flags;
- do not prepend `memos --help` when `get` is the already known goal.
2 changes: 2 additions & 0 deletions src/memos_cli/backend/memory_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ def get_memories(self, **kwargs: Any) -> dict[str, Any]:
raise APIError("Get memory requires user_id")

payload: dict[str, Any] = {"user_id": user_id}
if kwargs.get("conversation_id"):
payload["conversation_id"] = kwargs["conversation_id"]
if kwargs.get("page") is not None:
payload["page"] = kwargs["page"]
if kwargs.get("size") is not None:
Expand Down
26 changes: 26 additions & 0 deletions src/memos_cli/commands/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ def chat(
def get(
user_id_arg: str | None = typer.Argument(None, help="User ID"),
user_id: str | None = typer.Option(None, "--user-id", help="User ID"),
conversation_id: str | None = typer.Option(None, "--conversation-id", help="Conversation ID"),
page: int | None = typer.Option(None, "--page", min=1, help="Page number"),
size: int | None = typer.Option(None, "--size", min=1, help="Page size"),
include_preference: str | None = typer.Option(None, "--include-preference", help="Include preference memory: true or false"),
Expand All @@ -195,6 +196,31 @@ def get(
"""Get memories via the documented get_memory API."""
cmd_get(
user_id=user_id_arg or user_id,
conversation_id=conversation_id,
page=page,
size=size,
include_preference=include_preference,
include_tool_memory=include_tool_memory,
output_format=output_format,
detail=detail,
)


def list(
user_id_arg: str | None = typer.Argument(None, help="User ID"),
user_id: str | None = typer.Option(None, "--user-id", help="User ID"),
conversation_id: str | None = typer.Option(None, "--conversation-id", help="Conversation ID"),
page: int | None = typer.Option(None, "--page", min=1, help="Page number"),
size: int | None = typer.Option(None, "--size", min=1, help="Page size"),
include_preference: str | None = typer.Option(None, "--include-preference", help="Include preference memory: true or false"),
include_tool_memory: str | None = typer.Option(None, "--include-tool-memory", help="Include tool memory: true or false"),
output_format: str | None = typer.Option(None, "--format", help=FORMAT_HELP),
detail: str | None = typer.Option(None, "--detail", help=DETAIL_HELP),
):
"""List memories scoped to the current user/conversation (alias of `get`)."""
cmd_get(
user_id=user_id_arg or user_id,
conversation_id=conversation_id,
page=page,
size=size,
include_preference=include_preference,
Expand Down
17 changes: 15 additions & 2 deletions src/memos_cli/commands/memory_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,22 +612,35 @@ def cmd_chat(
def cmd_get(
*,
user_id: str | None,
conversation_id: str | None,
page: int | None,
size: int | None,
include_preference: str | None,
include_tool_memory: str | None,
output_format: str,
detail: str,
) -> None:
"""Execute get."""
"""Execute get.

The read path must use the same conversation scope as the write path
(``cmd_add``): the server stores memories keyed by ``conversation_id``,
so listing without one silently misses records added to the default
conversation.
"""
start_time = time.time()
final_output = resolve_output_format(output_format)
final_detail = validate_detail(detail)
try:
config, backend = _load_backend()
final_user_id = user_id or config.defaults.user_id
final_conversation_id = (
conversation_id
or config.defaults.conversation_id
or DEFAULT_CONVERSATION_ID
)
response = backend.get_memories(
user_id=final_user_id,
conversation_id=final_conversation_id,
page=page,
size=size,
include_preference=parse_bool_option(include_preference, option_name="--include-preference"),
Expand All @@ -645,7 +658,7 @@ def cmd_get(
data=memories,
duration_ms=duration_ms,
count=len(memories),
scope={"user_id": final_user_id},
scope={"user_id": final_user_id, "conversation_id": final_conversation_id},
detail=final_detail,
)
return
Expand Down
4 changes: 3 additions & 1 deletion src/memos_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from memos_cli.completion import register_completion_compat
from memos_cli.commands.init import init_cmd, uninstall_cmd
from memos_cli.commands.config_cmd import config_app
from memos_cli.commands.memory import add, extract, feedback, rerank, search, chat, get, delete, origin
from memos_cli.commands.memory import add, extract, feedback, rerank, search, chat, get, list, delete, origin
from memos_cli.commands.message import message, status
from memos_cli.commands.kb import kb_app
from memos_cli.state import set_runtime_options
Expand All @@ -34,6 +34,7 @@ class CommandFirstTyperGroup(TyperGroup):
"add",
"search",
"get",
"list",
"origin",
"delete",
"extract",
Expand Down Expand Up @@ -136,6 +137,7 @@ def _fire_telemetry(command_name: str, extra: dict | None = None):
app.command(rich_help_panel="Memory Operations")(feedback)
app.command(rich_help_panel="Memory Operations")(search)
app.command(rich_help_panel="Memory Operations")(get)
app.command(rich_help_panel="Memory Operations")(list)
app.command(rich_help_panel="Memory Operations")(origin)
app.command(rich_help_panel="Memory Operations")(delete)

Expand Down
7 changes: 6 additions & 1 deletion src/memos_cli/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from rich.table import Table
from rich.text import Text

from memos_cli.backend.normalizers import build_skill_memory_text
from memos_cli.backend.normalizers import build_skill_memory_text, extract_memory_list
from memos_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR


Expand Down Expand Up @@ -62,6 +62,11 @@ def extract_memory_records_from_response(data: dict[str, Any], *, detail: str =
items.extend(("memory", item) for item in raw_data["memories"] if isinstance(item, dict))
if not items and any(key in raw_data for key in ("id", "memory", "text", "memory_value", "content")):
items.append(("memory", raw_data))
# The official get_memory API may wrap records in a `text_mem` bucket
# envelope; parse it the same way `extract_memory_list` does so list/get
# do not silently render zero records.
if not items and isinstance(raw_data.get("text_mem"), list):
items.extend(("memory", item) for item in extract_memory_list(raw_data) if isinstance(item, dict))
elif isinstance(data, dict) and isinstance(data.get("results"), list):
items = [("memory", item) for item in data["results"] if isinstance(item, dict)]

Expand Down
14 changes: 14 additions & 0 deletions tests/test_memory_api_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ def test_search_memories_uses_documented_endpoint_only(self) -> None:
self.assertEqual(transport.calls[0][1], "/search/memory")
self.assertEqual(transport.calls[0][2]["json_body"]["knowledgebase_ids"], ["base123"])

def test_get_memories_forwards_conversation_id(self) -> None:
transport = RecordingTransport()
api = MemoryAPI(transport)

api.get_memories(user_id="user_1", conversation_id="conv_1", page=2, size=25)

self.assertEqual(len(transport.calls), 1)
self.assertEqual(transport.calls[0][1], "/get/memory")
body = transport.calls[0][2]["json_body"]
self.assertEqual(body["user_id"], "user_1")
self.assertEqual(body["conversation_id"], "conv_1")
self.assertEqual(body["page"], 2)
self.assertEqual(body["size"], 25)


if __name__ == "__main__":
unittest.main()
146 changes: 146 additions & 0 deletions tests/test_memory_get_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
from __future__ import annotations

import unittest
from unittest.mock import patch

from memos_cli.commands import memory, memory_cmd
from memos_cli.config import MemOSConfig, PlatformConfig
from memos_cli.output import extract_memory_records_from_response


class MemoryGetScopeTests(unittest.TestCase):
"""The get/list read path must use the same conversation scope as add."""

def test_cmd_get_forwards_conversation_id_to_backend(self) -> None:
config = MemOSConfig(
platform=PlatformConfig(
api_key="test-key",
base_url="https://example.test/api",
)
)
config.defaults.user_id = "user_1"
config.defaults.conversation_id = "conversation_1"

captured: dict = {}

class Backend:
def get_memories(self, **kwargs):
captured.update(kwargs)
return {"data": {"text_mem": []}}

with patch.object(memory_cmd, "_load_backend", return_value=(config, Backend())):
with patch.object(memory_cmd, "format_json"):
memory_cmd.cmd_get(
user_id=None,
conversation_id=None,
page=1,
size=50,
include_preference=None,
include_tool_memory=None,
output_format="json",
detail="simple",
)

self.assertEqual(captured["user_id"], "user_1")
self.assertEqual(captured["conversation_id"], "conversation_1")

def test_cmd_get_prefers_explicit_conversation_id(self) -> None:
config = MemOSConfig(
platform=PlatformConfig(
api_key="test-key",
base_url="https://example.test/api",
)
)
config.defaults.user_id = "user_1"
config.defaults.conversation_id = "conversation_1"

captured: dict = {}

class Backend:
def get_memories(self, **kwargs):
captured.update(kwargs)
return {"data": {"text_mem": []}}

with patch.object(memory_cmd, "_load_backend", return_value=(config, Backend())):
with patch.object(memory_cmd, "format_json"):
memory_cmd.cmd_get(
user_id="user_2",
conversation_id="conv_explicit",
page=1,
size=50,
include_preference=None,
include_tool_memory=None,
output_format="json",
detail="simple",
)

self.assertEqual(captured["user_id"], "user_2")
self.assertEqual(captured["conversation_id"], "conv_explicit")

def test_get_entrypoint_passes_conversation_id(self) -> None:
with patch.object(memory, "cmd_get") as cmd_get:
memory.get(
user_id_arg=None,
user_id="user_1",
conversation_id="conv_1",
page=2,
size=25,
include_preference="true",
include_tool_memory="false",
output_format="json",
detail="detail",
)

self.assertEqual(cmd_get.call_count, 1)
self.assertEqual(cmd_get.call_args.kwargs["conversation_id"], "conv_1")
self.assertEqual(cmd_get.call_args.kwargs["user_id"], "user_1")

def test_list_entrypoint_delegates_to_cmd_get(self) -> None:
with patch.object(memory, "cmd_get") as cmd_get:
memory.list(
user_id_arg="user_1",
user_id=None,
conversation_id="conv_1",
page=1,
size=10,
include_preference=None,
include_tool_memory=None,
output_format="table",
detail="simple",
)

self.assertEqual(cmd_get.call_count, 1)
self.assertEqual(cmd_get.call_args.kwargs["conversation_id"], "conv_1")
self.assertEqual(cmd_get.call_args.kwargs["user_id"], "user_1")


class GetMemoryTextMemParsingTests(unittest.TestCase):
"""list/get must not silently drop records served via the text_mem envelope."""

def test_extract_records_reads_text_mem_bucket(self) -> None:
response = {
"code": 0,
"data": {
"text_mem": [
{
"session_id": "s1",
"conversation_id": "conv_1",
"memories": [
{"id": "mem_1", "memory": "User likes coffee"},
{"id": "mem_2", "text": "Deployment is at 3pm"},
],
}
]
},
}

records = extract_memory_records_from_response(response, detail="simple")

self.assertEqual(len(records), 2)
contents = {record.get("memory") for record in records}
self.assertIn("User likes coffee", contents)
self.assertIn("Deployment is at 3pm", contents)


if __name__ == "__main__":
unittest.main()
Loading