memorytalk 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memorytalk-0.4.0 → memorytalk-0.4.2}/PKG-INFO +2 -1
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/__init__.py +9 -2
- memorytalk-0.4.2/memorytalk/api/recall.py +18 -0
- memorytalk-0.4.2/memorytalk/api/review.py +36 -0
- memorytalk-0.4.2/memorytalk/api/tags.py +71 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/__init__.py +5 -2
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/_format.py +109 -4
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/_http.py +3 -2
- memorytalk-0.4.2/memorytalk/cli/filter.py +402 -0
- memorytalk-0.4.2/memorytalk/cli/recall.py +122 -0
- memorytalk-0.4.2/memorytalk/cli/review.py +63 -0
- memorytalk-0.4.2/memorytalk/cli/setup/__init__.py +146 -0
- memorytalk-0.4.2/memorytalk/cli/setup/steps/claude_hook.py +138 -0
- memorytalk-0.4.2/memorytalk/cli/setup/steps/embedding.py +200 -0
- memorytalk-0.4.2/memorytalk/cli/setup/steps/path_takeover.py +208 -0
- memorytalk-0.4.2/memorytalk/cli/setup/steps/provider.py +18 -0
- memorytalk-0.4.2/memorytalk/cli/setup/steps/server.py +71 -0
- memorytalk-0.4.2/memorytalk/cli/setup/summary.py +128 -0
- memorytalk-0.4.2/memorytalk/cli/setup/venv.py +117 -0
- memorytalk-0.4.2/memorytalk/cli/setup/wizard.py +117 -0
- memorytalk-0.4.2/memorytalk/cli/tag.py +88 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/config.py +40 -7
- memorytalk-0.4.2/memorytalk/filters/new-session/filter.py +28 -0
- memorytalk-0.4.2/memorytalk/filters/new-session/meta.json +5 -0
- memorytalk-0.4.2/memorytalk/provider/__init__.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/provider/embedding.py +9 -16
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/repository/__init__.py +4 -1
- memorytalk-0.4.2/memorytalk/repository/recall.py +255 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/repository/schema.py +38 -1
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/repository/sessions.py +4 -14
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/repository/store.py +5 -1
- memorytalk-0.4.2/memorytalk/repository/tags.py +134 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/__init__.py +14 -2
- memorytalk-0.4.2/memorytalk/schemas/recall.py +23 -0
- memorytalk-0.4.2/memorytalk/schemas/review.py +42 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/search.py +2 -1
- memorytalk-0.4.2/memorytalk/schemas/tags.py +26 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/view.py +3 -1
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/service/__init__.py +4 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/service/cards.py +4 -1
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/service/rebuild.py +41 -3
- memorytalk-0.4.2/memorytalk/service/recall.py +125 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/service/search.py +7 -3
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/service/sessions.py +35 -55
- memorytalk-0.4.2/memorytalk/service/tags.py +162 -0
- memorytalk-0.4.2/memorytalk/util/__init__.py +0 -0
- memorytalk-0.4.2/memorytalk/util/console.py +156 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/util/dsl.py +17 -6
- memorytalk-0.4.2/memorytalk/util/env_template.py +45 -0
- memorytalk-0.4.2/memorytalk/util/settings_io.py +52 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk.egg-info/PKG-INFO +2 -1
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk.egg-info/SOURCES.txt +27 -2
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk.egg-info/requires.txt +1 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/pyproject.toml +8 -1
- memorytalk-0.4.0/memorytalk/api/tags.py +0 -30
- memorytalk-0.4.0/memorytalk/cli/_setup_helpers.py +0 -157
- memorytalk-0.4.0/memorytalk/cli/setup.py +0 -429
- memorytalk-0.4.0/memorytalk/cli/tag.py +0 -51
- memorytalk-0.4.0/memorytalk/schemas/tags.py +0 -14
- {memorytalk-0.4.0 → memorytalk-0.4.2}/LICENSE +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/README.md +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/__init__.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/__main__.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/adapters/__init__.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/adapters/base.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/adapters/claude_code.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/cards.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/links.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/log.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/rebuild.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/search.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/sessions.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/status.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/api/view.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/_render.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/card.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/link.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/log.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/rebuild.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/search.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/server.py +0 -0
- {memorytalk-0.4.0/memorytalk/provider → memorytalk-0.4.2/memorytalk/cli/setup/steps}/__init__.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/sync.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/cli/view.py +0 -0
- {memorytalk-0.4.0/memorytalk/util → memorytalk-0.4.2/memorytalk/filters}/__init__.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/provider/lancedb.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/provider/storage.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/repository/cards.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/repository/links.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/repository/search_log.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/cards.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/links.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/log.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/rebuild.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/sessions.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/shared.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/schemas/status.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/service/events.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/service/links.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/util/ids.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/util/snippet.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk/util/ttl.py +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk.egg-info/dependency_links.txt +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk.egg-info/entry_points.txt +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/memorytalk.egg-info/top_level.txt +0 -0
- {memorytalk-0.4.0 → memorytalk-0.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memorytalk
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Persistent cross-session memory for AI agents via Talk-Card architecture (v2)
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -19,6 +19,7 @@ Requires-Dist: pyarrow>=14.0.0
|
|
|
19
19
|
Requires-Dist: aiosqlite>=0.19.0
|
|
20
20
|
Requires-Dist: aiofiles>=23.0.0
|
|
21
21
|
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: questionary>=2.0.0
|
|
22
23
|
Provides-Extra: dev
|
|
23
24
|
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
24
25
|
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
@@ -15,7 +15,7 @@ from memorytalk.provider.embedding import (
|
|
|
15
15
|
)
|
|
16
16
|
from memorytalk.service import (
|
|
17
17
|
CardService, EventWriter, LinkService, RebuildService,
|
|
18
|
-
SearchService, SessionService,
|
|
18
|
+
RecallService, SearchService, SessionService, TagService,
|
|
19
19
|
)
|
|
20
20
|
from memorytalk.provider.lancedb import LanceStore
|
|
21
21
|
from memorytalk.provider.storage import LocalStorage
|
|
@@ -52,8 +52,12 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
52
52
|
app.state.db = db
|
|
53
53
|
app.state.vectors = vectors
|
|
54
54
|
app.state.embedder = embedder
|
|
55
|
+
# TagService must be wired before SessionService (the latter holds
|
|
56
|
+
# a reference so ingest can stamp `sync_session` tags).
|
|
57
|
+
app.state.tags = TagService(db=db, storage=storage, events=events)
|
|
55
58
|
app.state.sessions = SessionService(
|
|
56
59
|
config=config, db=db, vectors=vectors, events=events,
|
|
60
|
+
tags=app.state.tags,
|
|
57
61
|
)
|
|
58
62
|
app.state.cards = CardService(
|
|
59
63
|
config=config, db=db, vectors=vectors, embedder=embedder, events=events,
|
|
@@ -62,6 +66,9 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
62
66
|
app.state.search = SearchService(
|
|
63
67
|
config=config, db=db, vectors=vectors, embedder=embedder,
|
|
64
68
|
)
|
|
69
|
+
app.state.recall = RecallService(
|
|
70
|
+
config=config, db=db, vectors=vectors, embedder=embedder,
|
|
71
|
+
)
|
|
65
72
|
app.state.rebuild = RebuildService(
|
|
66
73
|
config=config, db=db, vectors=vectors, embedder=embedder,
|
|
67
74
|
)
|
|
@@ -86,7 +93,7 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
86
93
|
from memorytalk.api.status import router as status_router
|
|
87
94
|
app.include_router(status_router, prefix="/v2")
|
|
88
95
|
|
|
89
|
-
for name in ("sessions", "cards", "links", "tags", "search", "view", "log", "rebuild"):
|
|
96
|
+
for name in ("sessions", "cards", "links", "tags", "search", "recall", "review", "view", "log", "rebuild"):
|
|
90
97
|
try:
|
|
91
98
|
mod = __import__(f"memorytalk.api.{name}", fromlist=["router"])
|
|
92
99
|
app.include_router(mod.router, prefix="/v2")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""POST /v2/recall."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
|
|
6
|
+
from memorytalk.schemas import RecallRequest, RecallResponse
|
|
7
|
+
from memorytalk.service.recall import RecallError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post("/recall", response_model=RecallResponse)
|
|
14
|
+
async def post_recall(payload: RecallRequest, request: Request):
|
|
15
|
+
try:
|
|
16
|
+
return await request.app.state.recall.recall(payload)
|
|
17
|
+
except RecallError as e:
|
|
18
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""GET /v2/review/list, GET /v2/review/detail/{session_id}.
|
|
2
|
+
|
|
3
|
+
Pure read-only: thin wrappers over RecallStore queries. No service-layer
|
|
4
|
+
class needed — review has no business logic, it's just SELECTs.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
9
|
+
|
|
10
|
+
from memorytalk.schemas import ReviewDetailResponse, ReviewListResponse
|
|
11
|
+
from memorytalk.util.ids import prefix_session_id
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/review/list", response_model=ReviewListResponse)
|
|
18
|
+
async def review_list(
|
|
19
|
+
request: Request,
|
|
20
|
+
limit: int = Query(100, gt=0, le=1000),
|
|
21
|
+
):
|
|
22
|
+
rows = await request.app.state.db.recall.list_sessions(limit=limit)
|
|
23
|
+
return ReviewListResponse(sessions=rows)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("/review/detail/{session_id}", response_model=ReviewDetailResponse)
|
|
27
|
+
async def review_detail(
|
|
28
|
+
session_id: str,
|
|
29
|
+
request: Request,
|
|
30
|
+
limit: int = Query(50, gt=0, le=500),
|
|
31
|
+
):
|
|
32
|
+
sid = prefix_session_id(session_id)
|
|
33
|
+
detail = await request.app.state.db.recall.session_detail(sid, limit=limit)
|
|
34
|
+
if detail is None:
|
|
35
|
+
raise HTTPException(status_code=404, detail="session not found in recall log")
|
|
36
|
+
return ReviewDetailResponse(**detail)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tag endpoints — resource-rooted (subject in URL path).
|
|
2
|
+
|
|
3
|
+
Sessions:
|
|
4
|
+
POST /v2/sessions/{session_id}/tags body: {"tags": ["k:v", ...]}
|
|
5
|
+
DELETE /v2/sessions/{session_id}/tags?key=k1&key=k2
|
|
6
|
+
|
|
7
|
+
Cards (commit 2 will enable):
|
|
8
|
+
POST /v2/cards/{card_id}/tags
|
|
9
|
+
DELETE /v2/cards/{card_id}/tags?key=...
|
|
10
|
+
|
|
11
|
+
Subject-id prefix validation happens inside TagService — the path param
|
|
12
|
+
just plumbs whatever string the URL had through. A wrong-prefix id
|
|
13
|
+
(e.g. ``sess_xxx`` posted to the cards route) returns 400 from the
|
|
14
|
+
service layer rather than a 404 from FastAPI routing.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
19
|
+
|
|
20
|
+
from memorytalk.schemas import TagsAddRequest, TagsResponse
|
|
21
|
+
from memorytalk.service import SessionNotFound, TagServiceError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
router = APIRouter()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _wrap_call(coro):
|
|
28
|
+
try:
|
|
29
|
+
return await coro
|
|
30
|
+
except SessionNotFound as e:
|
|
31
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
32
|
+
except TagServiceError as e:
|
|
33
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.post("/sessions/{session_id}/tags", response_model=TagsResponse)
|
|
37
|
+
async def post_session_tags(
|
|
38
|
+
session_id: str, payload: TagsAddRequest, request: Request,
|
|
39
|
+
) -> TagsResponse:
|
|
40
|
+
return await _wrap_call(
|
|
41
|
+
request.app.state.tags.add_tags(session_id, payload.tags)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.delete("/sessions/{session_id}/tags", response_model=TagsResponse)
|
|
46
|
+
async def delete_session_tags(
|
|
47
|
+
session_id: str, request: Request,
|
|
48
|
+
key: list[str] = Query(..., min_length=1),
|
|
49
|
+
) -> TagsResponse:
|
|
50
|
+
return await _wrap_call(
|
|
51
|
+
request.app.state.tags.remove_tags(session_id, key)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post("/cards/{card_id}/tags", response_model=TagsResponse)
|
|
56
|
+
async def post_card_tags(
|
|
57
|
+
card_id: str, payload: TagsAddRequest, request: Request,
|
|
58
|
+
) -> TagsResponse:
|
|
59
|
+
return await _wrap_call(
|
|
60
|
+
request.app.state.tags.add_tags(card_id, payload.tags)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.delete("/cards/{card_id}/tags", response_model=TagsResponse)
|
|
65
|
+
async def delete_card_tags(
|
|
66
|
+
card_id: str, request: Request,
|
|
67
|
+
key: list[str] = Query(..., min_length=1),
|
|
68
|
+
) -> TagsResponse:
|
|
69
|
+
return await _wrap_call(
|
|
70
|
+
request.app.state.tags.remove_tags(card_id, key)
|
|
71
|
+
)
|
|
@@ -13,9 +13,12 @@ def main() -> None:
|
|
|
13
13
|
main.add_command(server)
|
|
14
14
|
|
|
15
15
|
# Other command groups are attached as they're implemented.
|
|
16
|
-
for _name in ("card", "tag", "link", "sync", "search", "view", "log", "rebuild", "setup"):
|
|
16
|
+
for _name in ("card", "tag", "link", "sync", "search", "recall", "review", "view", "log", "rebuild", "setup", "filter"):
|
|
17
17
|
try:
|
|
18
18
|
_mod = __import__(f"memorytalk.cli.{_name}", fromlist=[_name])
|
|
19
|
-
|
|
19
|
+
# `filter` shadows a builtin → the command object is named `filter_`
|
|
20
|
+
# in the module; everything else uses the same name as the module.
|
|
21
|
+
attr = "filter_" if _name == "filter" else _name
|
|
22
|
+
main.add_command(getattr(_mod, attr))
|
|
20
23
|
except ImportError:
|
|
21
24
|
pass
|
|
@@ -141,6 +141,10 @@ def fmt_view_card(resp: dict) -> str:
|
|
|
141
141
|
if card.get("summary"):
|
|
142
142
|
out.append(f"**Summary:** {card['summary']}")
|
|
143
143
|
out.append("")
|
|
144
|
+
tags = card.get("tags") or []
|
|
145
|
+
if tags:
|
|
146
|
+
out.append("**Tags:** " + ", ".join(f"`{_tag_label(t)}`" for t in tags))
|
|
147
|
+
out.append("")
|
|
144
148
|
|
|
145
149
|
links = resp.get("links") or []
|
|
146
150
|
out.append(f"## links ({len(links)})")
|
|
@@ -180,7 +184,7 @@ def fmt_view_session(resp: dict) -> str:
|
|
|
180
184
|
out.append("")
|
|
181
185
|
tags = sess.get("tags") or []
|
|
182
186
|
if tags:
|
|
183
|
-
out.append("**Tags:** " + ", ".join(f"`{t}`" for t in tags))
|
|
187
|
+
out.append("**Tags:** " + ", ".join(f"`{_tag_label(t)}`" for t in tags))
|
|
184
188
|
out.append("")
|
|
185
189
|
metadata = sess.get("metadata") or {}
|
|
186
190
|
if metadata:
|
|
@@ -247,8 +251,18 @@ def _detail_summary(kind: str, detail: dict) -> str:
|
|
|
247
251
|
if kind == "rounds_overwrite_skipped":
|
|
248
252
|
idxs = detail.get("indexes", [])
|
|
249
253
|
return f"indexes={','.join(str(i) for i in idxs)}"
|
|
250
|
-
if kind
|
|
251
|
-
|
|
254
|
+
if kind in ("tag_added", "tag_removed"):
|
|
255
|
+
key = detail.get("key", "")
|
|
256
|
+
value = detail.get("value", "") or ""
|
|
257
|
+
label = f"{key}:{value}" if value else key
|
|
258
|
+
return f"`{label}`"
|
|
259
|
+
if kind == "tag_updated":
|
|
260
|
+
key = detail.get("key", "")
|
|
261
|
+
value = detail.get("value", "") or ""
|
|
262
|
+
prior = detail.get("prior_value", "") or ""
|
|
263
|
+
new_label = f"{key}:{value}" if value else key
|
|
264
|
+
old_label = f"{key}:{prior}" if prior else key
|
|
265
|
+
return f"`{old_label}` → `{new_label}`"
|
|
252
266
|
if kind == "card_extracted":
|
|
253
267
|
return f"`{detail.get('card_id', '')}` · indexes={detail.get('indexes', '')}"
|
|
254
268
|
if kind == "linked":
|
|
@@ -321,6 +335,90 @@ def fmt_log(resp: dict) -> str:
|
|
|
321
335
|
return fmt_error(f"unknown log type: {resp.get('type')!r}")
|
|
322
336
|
|
|
323
337
|
|
|
338
|
+
# ---------- review ----------
|
|
339
|
+
|
|
340
|
+
def fmt_review_list(resp: dict) -> str:
|
|
341
|
+
sessions = resp.get("sessions") or []
|
|
342
|
+
out: list[str] = []
|
|
343
|
+
out.append(f"# Sessions with recall history ({len(sessions)})")
|
|
344
|
+
out.append("")
|
|
345
|
+
if not sessions:
|
|
346
|
+
out.append("*(no recall history yet — call `memory-talk recall <session_id> <prompt>` first)*")
|
|
347
|
+
out.append("")
|
|
348
|
+
return _join(*out)
|
|
349
|
+
for s in sessions:
|
|
350
|
+
sid = s.get("session_id", "")
|
|
351
|
+
exist = "true" if s.get("session_exist") else "false"
|
|
352
|
+
rc = s.get("round_count", 0)
|
|
353
|
+
ci = s.get("cards_injected", 0)
|
|
354
|
+
last = s.get("last_at", "")
|
|
355
|
+
last_q = (s.get("last_query") or "").replace("\n", " ").strip()
|
|
356
|
+
out.append(
|
|
357
|
+
f"- **`{sid}`** · `session_exist={exist}` · {rc} rounds · "
|
|
358
|
+
f"{ci} cards · last {last}"
|
|
359
|
+
)
|
|
360
|
+
if last_q:
|
|
361
|
+
out.append(f" > {last_q}")
|
|
362
|
+
out.append("")
|
|
363
|
+
return _join(*out)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def fmt_review_detail(resp: dict) -> str:
|
|
367
|
+
sid = resp.get("session_id", "")
|
|
368
|
+
exist = "true" if resp.get("session_exist") else "false"
|
|
369
|
+
out: list[str] = []
|
|
370
|
+
out.append(f"# `{sid}` · session_exist={exist}")
|
|
371
|
+
out.append("")
|
|
372
|
+
out.append(
|
|
373
|
+
f"{resp.get('round_count', 0)} rounds · "
|
|
374
|
+
f"{resp.get('cards_injected', 0)} cards (deduped) · "
|
|
375
|
+
f"first {resp.get('first_at', '')} · last {resp.get('last_at', '')}"
|
|
376
|
+
)
|
|
377
|
+
out.append("")
|
|
378
|
+
rounds = resp.get("rounds") or []
|
|
379
|
+
for i, r in enumerate(rounds):
|
|
380
|
+
if i > 0:
|
|
381
|
+
out.append("---")
|
|
382
|
+
out.append("")
|
|
383
|
+
rc = r.get("round_count", "?")
|
|
384
|
+
rec_at = r.get("recalled_at", "")
|
|
385
|
+
out.append(f"## Round {rc} · {rec_at}")
|
|
386
|
+
out.append("")
|
|
387
|
+
q = (r.get("query") or "").replace("\n", " ").strip()
|
|
388
|
+
if q:
|
|
389
|
+
out.append(f"> {q}")
|
|
390
|
+
out.append("")
|
|
391
|
+
for hit in r.get("hits") or []:
|
|
392
|
+
cid = hit.get("card_id", "")
|
|
393
|
+
summary = (hit.get("summary") or "").replace("\n", " ").strip()
|
|
394
|
+
line = f"{hit.get('rank', '?')}. `{cid}`"
|
|
395
|
+
if summary:
|
|
396
|
+
line += f" — {summary}"
|
|
397
|
+
out.append(line)
|
|
398
|
+
out.append("")
|
|
399
|
+
return _join(*out)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ---------- recall ----------
|
|
403
|
+
|
|
404
|
+
def fmt_recall(resp: dict) -> str:
|
|
405
|
+
"""Bash code-block style — designed to be inlined into LLM context.
|
|
406
|
+
|
|
407
|
+
Empty hit set → empty string (per docs: no '## Memory recall (0)'
|
|
408
|
+
placeholder; the harness gets nothing to inject).
|
|
409
|
+
"""
|
|
410
|
+
hits = resp.get("recalled") or []
|
|
411
|
+
if not hits:
|
|
412
|
+
return ""
|
|
413
|
+
lines = ["```bash", "# Relevant memories — run any to expand detail:"]
|
|
414
|
+
for h in hits:
|
|
415
|
+
cid = h.get("card_id", "")
|
|
416
|
+
summary = (h.get("summary") or "").replace("\n", " ").strip()
|
|
417
|
+
lines.append(f"memory-talk view {cid} # {summary}")
|
|
418
|
+
lines.append("```")
|
|
419
|
+
return _join(*lines)
|
|
420
|
+
|
|
421
|
+
|
|
324
422
|
# ---------- write commands (single line ok) ----------
|
|
325
423
|
|
|
326
424
|
def fmt_card_create(resp: dict) -> str:
|
|
@@ -331,11 +429,18 @@ def fmt_link_create(resp: dict) -> str:
|
|
|
331
429
|
return f"ok: linked `{resp.get('link_id', '')}`\n"
|
|
332
430
|
|
|
333
431
|
|
|
432
|
+
def _tag_label(pair: dict) -> str:
|
|
433
|
+
"""Render `{"key": "k", "value": "v"}` as ``k:v`` (or just ``k`` if value is empty)."""
|
|
434
|
+
key = pair.get("key", "")
|
|
435
|
+
value = pair.get("value", "") or ""
|
|
436
|
+
return f"{key}:{value}" if value else key
|
|
437
|
+
|
|
438
|
+
|
|
334
439
|
def fmt_tag(resp: dict) -> str:
|
|
335
440
|
tags = resp.get("tags") or []
|
|
336
441
|
if not tags:
|
|
337
442
|
return "ok: tags = *(empty)*\n"
|
|
338
|
-
return "ok: tags = " + ", ".join(f"`{
|
|
443
|
+
return "ok: tags = " + ", ".join(f"`{_tag_label(p)}`" for p in tags) + "\n"
|
|
339
444
|
|
|
340
445
|
|
|
341
446
|
# ---------- sync / rebuild ----------
|
|
@@ -57,13 +57,14 @@ _make_client: Optional[Callable[[Config], httpx.Client]] = None
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def api(method: str, path: str, config: Config,
|
|
60
|
-
json_body: dict | None = None, timeout: float = 30.0
|
|
60
|
+
json_body: dict | None = None, timeout: float = 30.0,
|
|
61
|
+
params: dict | list[tuple[str, str]] | None = None) -> dict:
|
|
61
62
|
factory = _make_client or _default_client
|
|
62
63
|
client = factory(config)
|
|
63
64
|
# No `with` — ASGI test transport has no context-manager support, and
|
|
64
65
|
# the CLI is a short-lived process where leaked TCP sockets get reaped
|
|
65
66
|
# at exit. Tests share a long-lived ASGI client across calls.
|
|
66
|
-
resp = client.request(method, path, json=json_body, timeout=timeout)
|
|
67
|
+
resp = client.request(method, path, json=json_body, timeout=timeout, params=params)
|
|
67
68
|
if resp.status_code >= 400:
|
|
68
69
|
try:
|
|
69
70
|
payload = resp.json()
|