thread-recall 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,10 @@
1
+ """thread-recall: governed, on-prem conversation memory for AI agents.
2
+
3
+ Per-thread history (recency) and semantic recall (nearest-neighbour over
4
+ embeddings), with optional PII masking on write and tamper-evident audit.
5
+ Part of the Governed Agent Stack.
6
+ """
7
+ from thread_recall.store import Memory, Turn
8
+
9
+ __version__ = "0.1.0"
10
+ __all__ = ["Memory", "Turn", "__version__"]
thread_recall/cli.py ADDED
@@ -0,0 +1,46 @@
1
+ """Command line: a quick zero-config demo of governed agent memory."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+
6
+ from thread_recall import __version__
7
+ from thread_recall.store import Memory
8
+
9
+
10
+ def cmd_demo(_args) -> int:
11
+ mem = Memory(":memory:")
12
+ thread = "demo-conversation"
13
+
14
+ # toy 3-dim embeddings just to show semantic recall without a model
15
+ mem.remember(thread, "user", "How much MRR did the pro plan make?", embedding=[1, 0, 0])
16
+ mem.remember(thread, "assistant", "Pro plan MRR was 297.", embedding=[0.9, 0.1, 0])
17
+ mem.remember(thread, "user", "What's the weather like?", embedding=[0, 0, 1])
18
+
19
+ print("thread-recall demo (in-memory SQLite, no model)\n")
20
+ print("Recent turns (chronological):")
21
+ for t in mem.recent(thread):
22
+ print(f" [{t.role}] {t.content}")
23
+
24
+ print("\nSemantic recall for a revenue-ish query [1,0,0] (top 2):")
25
+ for t in mem.search(thread, [1, 0, 0], k=2):
26
+ print(f" {t.score:.2f} [{t.role}] {t.content}")
27
+
28
+ print("\nWith mask=True (pii-veil, if installed) content is scrubbed before")
29
+ print("storage; with audit=True every write lands in an agent-blackbox ledger.")
30
+ mem.close()
31
+ return 0
32
+
33
+
34
+ def main() -> None:
35
+ parser = argparse.ArgumentParser(prog="thread-recall", description=__doc__)
36
+ parser.add_argument("--version", action="version", version=f"thread-recall {__version__}")
37
+ sub = parser.add_subparsers(dest="cmd")
38
+ sub.add_parser("demo", help="run a zero-config in-memory demo")
39
+ args = parser.parse_args()
40
+ if args.cmd == "demo":
41
+ raise SystemExit(cmd_demo(args))
42
+ parser.print_help()
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
@@ -0,0 +1,157 @@
1
+ """thread-recall-mcp: expose governed agent memory as MCP tools.
2
+
3
+ An agent calls ``remember`` to store a turn and ``recall`` to retrieve relevant
4
+ past turns, instead of holding unbounded raw history in its context. The
5
+ governance is on the write side: PII is masked before anything is stored (via
6
+ pii-veil when installed), so the long-term memory never retains raw PII, and
7
+ every write can be mirrored into a tamper-evident audit ledger.
8
+
9
+ Isolation: every thread is namespaced by an ``actor`` -- the authenticated
10
+ principal. A caller can only reach threads under its own actor, so a raw
11
+ ``thread_id`` is no longer a key into anyone else's memory (no cross-thread
12
+ read, poison, or mass-delete). The governed gateway sets ``actor`` from the
13
+ caller's token and does not let the client override it; standalone, set
14
+ ``THREAD_RECALL_ACTOR`` (or pass ``actor``), else all callers share one
15
+ namespace. The caller always sees its own plain ``thread_id`` back.
16
+
17
+ Configuration (environment):
18
+ THREAD_RECALL_DB SQLite path for the store (default: logs/recall-mem.db)
19
+ THREAD_RECALL_MASK mask PII on write (default on; set 0 to disable)
20
+ THREAD_RECALL_AUDIT mirror writes to agent-blackbox (default off; set 1 on)
21
+ THREAD_RECALL_ACTOR default principal when no actor is passed (default: shared)
22
+ THREAD_RECALL_EMBED hashing (default) | ollama
23
+ THREAD_RECALL_OLLAMA_* HOST / MODEL overrides for the Ollama embedder
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ import math
30
+ import os
31
+ import re
32
+ from typing import Any
33
+
34
+ from fastmcp import FastMCP
35
+
36
+ from thread_recall.store import Memory
37
+
38
+ _TOKEN = re.compile(r"[a-z0-9]+")
39
+ _ON = {"1", "true", "yes", "on"}
40
+ _SEP = "\x1f" # unit separator: partitions the store by actor, unseen by callers
41
+
42
+
43
+ def _principal(actor: str | None) -> str:
44
+ """The authenticated identity a thread belongs to. The gateway supplies it."""
45
+ return (actor or os.environ.get("THREAD_RECALL_ACTOR", "shared")).strip() or "shared"
46
+
47
+
48
+ def _key(actor: str | None, thread_id: str) -> str:
49
+ """Namespace a thread by its actor so ids cannot collide across principals."""
50
+ return f"{_principal(actor)}{_SEP}{thread_id}"
51
+
52
+
53
+ def _plain(namespaced: str) -> str:
54
+ """Strip the actor namespace so the caller sees the thread_id it passed in."""
55
+ return namespaced.split(_SEP, 1)[1] if _SEP in namespaced else namespaced
56
+
57
+
58
+ def _view(turn: dict) -> dict:
59
+ turn["thread_id"] = _plain(turn.get("thread_id", ""))
60
+ return turn
61
+
62
+
63
+ def _hash_embed(text: str, dim: int = 256) -> list[float]:
64
+ """Deterministic, offline bag-of-words embedding (no model, no network)."""
65
+ vec = [0.0] * dim
66
+ for tok in _TOKEN.findall(text.lower()):
67
+ vec[int(hashlib.md5(tok.encode()).hexdigest(), 16) % dim] += 1.0
68
+ norm = math.sqrt(sum(v * v for v in vec)) or 1.0
69
+ return [v / norm for v in vec]
70
+
71
+
72
+ def _embed(text: str) -> list[float]:
73
+ if os.environ.get("THREAD_RECALL_EMBED", "hashing").strip().lower() == "ollama":
74
+ import json
75
+ import urllib.request
76
+
77
+ host = os.environ.get("THREAD_RECALL_OLLAMA_HOST", "http://localhost:11434").rstrip("/")
78
+ model = os.environ.get("THREAD_RECALL_OLLAMA_MODEL", "nomic-embed-text")
79
+ req = urllib.request.Request(
80
+ f"{host}/api/embeddings",
81
+ data=json.dumps({"model": model, "prompt": text}).encode(),
82
+ headers={"Content-Type": "application/json"},
83
+ )
84
+ with urllib.request.urlopen(req) as resp:
85
+ return list(json.loads(resp.read())["embedding"])
86
+ return _hash_embed(text)
87
+
88
+
89
+ def _build_memory() -> Memory:
90
+ return Memory(
91
+ os.environ.get("THREAD_RECALL_DB", "logs/recall-mem.db"),
92
+ mask=os.environ.get("THREAD_RECALL_MASK", "1").strip().lower() in _ON,
93
+ audit=os.environ.get("THREAD_RECALL_AUDIT", "0").strip().lower() in _ON,
94
+ )
95
+
96
+
97
+ mcp = FastMCP("thread-recall")
98
+ _mem = _build_memory()
99
+
100
+
101
+ @mcp.tool
102
+ def remember(thread_id: str, content: str, role: str = "user",
103
+ actor: str | None = None) -> dict[str, Any]:
104
+ """Store one turn of a thread's memory. PII is masked before storage.
105
+
106
+ Args:
107
+ thread_id: The conversation/thread this turn belongs to.
108
+ content: The text to remember. Masked on write if masking is enabled.
109
+ role: Who said it (user / assistant / system).
110
+ actor: Authenticated principal owning the thread. Set by the gateway;
111
+ do not rely on client-supplied values for isolation.
112
+
113
+ Returns:
114
+ The stored turn's id. The content held in memory is the masked form.
115
+ """
116
+ turn_id = _mem.remember(_key(actor, thread_id), role, content, embedding=_embed(content))
117
+ return {"id": turn_id, "thread_id": thread_id, "stored": True}
118
+
119
+
120
+ @mcp.tool
121
+ def recall(thread_id: str, query: str, k: int = 5,
122
+ actor: str | None = None) -> dict[str, Any]:
123
+ """Recall the turns most relevant to a query, scoped to one thread.
124
+
125
+ Semantic nearest-neighbour over the actor's own thread. Only this actor's
126
+ copy of the thread is searched; another principal's memory is unreachable.
127
+ """
128
+ key = _key(actor, thread_id)
129
+ hits = _mem.search(key, _embed(query), k)
130
+ if not hits: # nothing embedded yet -> fall back to recency
131
+ hits = _mem.recent(key, k)
132
+ return {"count": len(hits), "results": [_view(t.as_dict()) for t in hits]}
133
+
134
+
135
+ @mcp.tool
136
+ def recent(thread_id: str, k: int = 10, actor: str | None = None) -> dict[str, Any]:
137
+ """The last k turns of the actor's thread, in chronological order."""
138
+ hits = _mem.recent(_key(actor, thread_id), k)
139
+ return {"count": len(hits), "results": [_view(t.as_dict()) for t in hits]}
140
+
141
+
142
+ @mcp.tool
143
+ def forget(thread_id: str, actor: str | None = None) -> dict[str, Any]:
144
+ """Delete the actor's copy of a thread. Returns the number of turns removed.
145
+
146
+ Only the calling principal's namespace is touched; a caller cannot wipe
147
+ another actor's thread even by naming the same thread_id.
148
+ """
149
+ return {"removed": _mem.forget(_key(actor, thread_id)), "thread_id": thread_id}
150
+
151
+
152
+ def main() -> None:
153
+ mcp.run()
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
thread_recall/store.py ADDED
@@ -0,0 +1,208 @@
1
+ """Governed, on-prem conversation memory for AI agents.
2
+
3
+ A small store for what an agent should remember across turns: per-thread
4
+ conversation history (recency) and optional semantic recall (nearest-neighbour
5
+ over stored embeddings). The "governed" part is the point — memory can be
6
+ PII-masked on write (so raw personal data never lands in the store) and every
7
+ write can be mirrored into a tamper-evident audit log.
8
+
9
+ The default backend is SQLite with embeddings kept as JSON and cosine computed
10
+ in Python: zero dependencies, works immediately, nothing leaves the building.
11
+ A Postgres + pgvector backend (for scale) is the next step; this module keeps
12
+ the embedding/scoring logic backend-agnostic so it drops in cleanly.
13
+
14
+ This is deliberately NOT a read path into your database — that is sql-steward's
15
+ job, and it stays read-only. This is the write-side state store that sits next
16
+ to it.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import math
22
+ import sqlite3
23
+ import threading
24
+ import time
25
+ from dataclasses import dataclass
26
+
27
+
28
+ @dataclass
29
+ class Turn:
30
+ """One remembered turn."""
31
+
32
+ id: int
33
+ thread_id: str
34
+ ts: float
35
+ role: str
36
+ content: str
37
+ metadata: dict | None = None
38
+ score: float | None = None # set by search()
39
+
40
+ def as_dict(self) -> dict:
41
+ d = {"id": self.id, "thread_id": self.thread_id, "ts": self.ts,
42
+ "role": self.role, "content": self.content, "metadata": self.metadata}
43
+ if self.score is not None:
44
+ d["score"] = self.score
45
+ return d
46
+
47
+
48
+ def _cosine(a: list[float], b: list[float]) -> float:
49
+ if not a or not b or len(a) != len(b):
50
+ return 0.0
51
+ dot = sum(x * y for x, y in zip(a, b))
52
+ na = math.sqrt(sum(x * x for x in a))
53
+ nb = math.sqrt(sum(y * y for y in b))
54
+ if na == 0.0 or nb == 0.0:
55
+ return 0.0
56
+ return dot / (na * nb)
57
+
58
+
59
+ class Memory:
60
+ """Thread-scoped agent memory.
61
+
62
+ Args:
63
+ path: SQLite path, or ":memory:" (default).
64
+ mask: scrub PII from content on write via pii-veil, if installed.
65
+ audit: mirror every write into an agent-blackbox ledger, if installed.
66
+ audit_db: path for the audit ledger (default "logs/recall.db").
67
+ """
68
+
69
+ def __init__(self, path: str = ":memory:", *, mask: bool = False,
70
+ audit: bool = False, audit_db: str = "logs/recall.db") -> None:
71
+ # check_same_thread=False + a lock so the store is usable from a server
72
+ # (e.g. the MCP server) that dispatches calls across worker threads.
73
+ self._conn = sqlite3.connect(path, check_same_thread=False)
74
+ self._conn.row_factory = sqlite3.Row
75
+ self._lock = threading.Lock()
76
+ self._init_db()
77
+ self._veil = _make_veil() if mask else None
78
+ self._ledger = _make_ledger(audit_db) if audit else None
79
+
80
+ def _init_db(self) -> None:
81
+ self._conn.execute(
82
+ """
83
+ CREATE TABLE IF NOT EXISTS memories (
84
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
85
+ thread_id TEXT NOT NULL,
86
+ ts REAL NOT NULL,
87
+ role TEXT NOT NULL,
88
+ content TEXT NOT NULL,
89
+ metadata TEXT,
90
+ embedding TEXT
91
+ )
92
+ """
93
+ )
94
+ self._conn.execute(
95
+ "CREATE INDEX IF NOT EXISTS idx_thread ON memories(thread_id, id)"
96
+ )
97
+ self._conn.commit()
98
+
99
+ # -- write --------------------------------------------------------------
100
+
101
+ def remember(
102
+ self,
103
+ thread_id: str,
104
+ role: str,
105
+ content: str,
106
+ *,
107
+ embedding: list[float] | None = None,
108
+ metadata: dict | None = None,
109
+ ) -> int:
110
+ """Store one turn. Returns its id. Content is PII-masked first if enabled."""
111
+ if self._veil is not None:
112
+ try:
113
+ content = self._veil.scrub_text(content)
114
+ except Exception:
115
+ pass
116
+ emb = json.dumps([float(x) for x in embedding]) if embedding else None
117
+ ts = time.time()
118
+ with self._lock:
119
+ cur = self._conn.execute(
120
+ "INSERT INTO memories (thread_id, ts, role, content, metadata, embedding)"
121
+ " VALUES (?, ?, ?, ?, ?, ?)",
122
+ (thread_id, ts, role, content, json.dumps(metadata) if metadata else None, emb),
123
+ )
124
+ self._conn.commit()
125
+ if self._ledger is not None:
126
+ try:
127
+ self._ledger.record(
128
+ actor="thread-recall", action="remember", target=thread_id,
129
+ meta={"role": role, "id": cur.lastrowid},
130
+ )
131
+ except Exception:
132
+ pass
133
+ return cur.lastrowid
134
+
135
+ def forget(self, thread_id: str) -> int:
136
+ """Delete a thread's memory. Returns rows removed."""
137
+ with self._lock:
138
+ cur = self._conn.execute("DELETE FROM memories WHERE thread_id = ?", (thread_id,))
139
+ self._conn.commit()
140
+ return cur.rowcount
141
+
142
+ # -- read ---------------------------------------------------------------
143
+
144
+ def recent(self, thread_id: str, k: int = 10) -> list[Turn]:
145
+ """The last k turns of a thread, in chronological order."""
146
+ with self._lock:
147
+ rows = self._conn.execute(
148
+ "SELECT * FROM memories WHERE thread_id = ? ORDER BY id DESC LIMIT ?",
149
+ (thread_id, k),
150
+ ).fetchall()
151
+ return [_row_to_turn(r) for r in reversed(rows)]
152
+
153
+ def search(self, thread_id: str, query_embedding: list[float], k: int = 5) -> list[Turn]:
154
+ """Semantic recall: the k turns whose embeddings are closest to the query."""
155
+ with self._lock:
156
+ rows = self._conn.execute(
157
+ "SELECT * FROM memories WHERE thread_id = ? AND embedding IS NOT NULL",
158
+ (thread_id,),
159
+ ).fetchall()
160
+ scored: list[Turn] = []
161
+ for r in rows:
162
+ turn = _row_to_turn(r)
163
+ turn.score = _cosine(query_embedding, json.loads(r["embedding"]))
164
+ scored.append(turn)
165
+ scored.sort(key=lambda t: t.score or 0.0, reverse=True)
166
+ return scored[:k]
167
+
168
+ def count(self, thread_id: str | None = None) -> int:
169
+ with self._lock:
170
+ if thread_id is None:
171
+ return self._conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]
172
+ return self._conn.execute(
173
+ "SELECT COUNT(*) FROM memories WHERE thread_id = ?", (thread_id,)
174
+ ).fetchone()[0]
175
+
176
+ def close(self) -> None:
177
+ self._conn.close()
178
+
179
+
180
+ def _row_to_turn(r: sqlite3.Row) -> Turn:
181
+ return Turn(
182
+ id=r["id"], thread_id=r["thread_id"], ts=r["ts"], role=r["role"],
183
+ content=r["content"],
184
+ metadata=json.loads(r["metadata"]) if r["metadata"] else None,
185
+ )
186
+
187
+
188
+ def _make_veil():
189
+ try:
190
+ from pii_veil import Veil
191
+
192
+ return Veil()
193
+ except Exception:
194
+ return None
195
+
196
+
197
+ def _make_ledger(path: str):
198
+ try:
199
+ import os
200
+
201
+ from agent_blackbox import Ledger
202
+
203
+ parent = os.path.dirname(path)
204
+ if parent:
205
+ os.makedirs(parent, exist_ok=True)
206
+ return Ledger(path)
207
+ except Exception:
208
+ return None
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: thread-recall
3
+ Version: 0.1.0
4
+ Summary: Governed, on-prem conversation memory for AI agents: per-thread history and semantic recall, with optional PII masking on write and tamper-evident audit. Zero-dependency SQLite by default.
5
+ Project-URL: Homepage, https://github.com/Pawansingh3889/thread-recall
6
+ Project-URL: Repository, https://github.com/Pawansingh3889/thread-recall
7
+ Project-URL: Governed Agent Stack, https://github.com/Pawansingh3889/governed-agent-stack
8
+ Author: Pawan Singh Kapkoti
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,ai,conversation,embeddings,governance,memory,on-prem,pgvector,pii,semantic-search,sqlite
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.10
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: ruff>=0.4; extra == 'dev'
26
+ Provides-Extra: mask
27
+ Requires-Dist: pii-veil>=0.1; extra == 'mask'
28
+ Provides-Extra: mcp
29
+ Requires-Dist: fastmcp>=2.0; extra == 'mcp'
30
+ Provides-Extra: postgres
31
+ Requires-Dist: pgvector>=0.2; extra == 'postgres'
32
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
33
+ Requires-Dist: sqlalchemy>=2.0; extra == 'postgres'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # thread-recall
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/thread-recall)](https://pypi.org/project/thread-recall/) [![Downloads](https://static.pepy.tech/badge/thread-recall)](https://pepy.tech/projects/thread-recall)
39
+
40
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
41
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
42
+
43
+ > Part of the [Governed Agent Stack](https://github.com/Pawansingh3889/governed-agent-stack): free, on-prem building blocks for an AI agent you can point at a real database and audit.
44
+
45
+ Governed, on-prem conversation memory for AI agents. It stores what an agent should remember across turns — per-thread history and optional semantic recall — and makes that memory **governable**: PII can be masked before it is ever written, and every write can be mirrored into a tamper-evident audit log.
46
+
47
+ The default backend is **SQLite with embeddings as JSON and cosine in Python**: zero dependencies, works immediately, nothing leaves the building. A Postgres + pgvector backend (for scale) is the next step; the scoring logic is backend-agnostic so it drops in cleanly.
48
+
49
+ This is the **write-side** companion to [sql-steward](https://github.com/Pawansingh3889/sql-steward). sql-steward is the read-only query gateway and stays read-only; thread-recall is where agent state lives, kept separate on purpose.
50
+
51
+ ## Quickstart
52
+
53
+ ```bash
54
+ pip install thread-recall
55
+ thread-recall demo
56
+ ```
57
+
58
+ ```python
59
+ from thread_recall import Memory
60
+
61
+ mem = Memory("agent.db") # or ":memory:"; mask=True / audit=True optional
62
+
63
+ mem.remember("thread-42", "user", "How much MRR did the pro plan make?", embedding=embed(q))
64
+ mem.remember("thread-42", "assistant", "Pro plan MRR was 297.")
65
+
66
+ mem.recent("thread-42", k=10) # last 10 turns, chronological
67
+ mem.search("thread-42", embed("revenue"), k=5) # nearest turns by embedding
68
+ ```
69
+
70
+ ## Why it's "governed"
71
+
72
+ - **PII masked on write.** `Memory(..., mask=True)` runs content through [pii-veil](https://github.com/Pawansingh3889/pii-veil) before it is stored, so raw personal data never lands in the memory store. No-op if pii-veil isn't installed.
73
+ - **Tamper-evident audit.** `Memory(..., audit=True)` mirrors every write into an [agent-blackbox](https://github.com/Pawansingh3889/agent-blackbox) hash-chained ledger, so "what did the agent choose to remember" has a checkable answer. No-op if agent-blackbox isn't installed.
74
+ - **On-prem by default.** SQLite file on your disk; embeddings generated by whatever local model you choose. Nothing is sent out.
75
+
76
+ ## API
77
+
78
+ | Method | Purpose |
79
+ |---|---|
80
+ | `remember(thread_id, role, content, embedding=None, metadata=None)` | Store a turn (PII-masked first if enabled). Returns its id. |
81
+ | `recent(thread_id, k=10)` | Last k turns, chronological. |
82
+ | `search(thread_id, query_embedding, k=5)` | Semantic recall: nearest turns by cosine over stored embeddings. |
83
+ | `forget(thread_id)` | Delete a thread's memory. |
84
+ | `count(thread_id=None)` | Turn count for a thread, or overall. |
85
+
86
+ Embeddings are supplied by the caller (generate them with any local model). thread-recall stores and scores them; it does not call out to any embedding service itself.
87
+
88
+ ## Roadmap
89
+
90
+ - **Postgres + pgvector backend** for scale and native nearest-neighbour (the SQLite backend computes cosine in Python, fine for thread-scoped recall, not for millions of rows).
91
+ - A thin LangGraph adapter, to use thread-recall alongside a LangGraph checkpointer.
92
+
93
+ ## Develop
94
+
95
+ ```bash
96
+ git clone https://github.com/Pawansingh3889/thread-recall
97
+ cd thread-recall
98
+ pip install -e ".[dev]"
99
+ pytest -q
100
+ ```
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,9 @@
1
+ thread_recall/__init__.py,sha256=j2Hcr3PdTs_rivaz16uDJtFWEC3RqjPr60c7zRW_cdA,369
2
+ thread_recall/cli.py,sha256=2wbj4_SkS16chNqGM-T-9neODg0FMXeG6jaZBeraUuk,1662
3
+ thread_recall/mcp_server.py,sha256=MBt4SGHiRc7CZ_auNVi_PzlarVro9hnBzwwJyiXUhBg,6099
4
+ thread_recall/store.py,sha256=Y3OCipi3fGKMNLjuQr9Ux4E_nRz4AhsW55NrETCsIVI,7210
5
+ thread_recall-0.1.0.dist-info/METADATA,sha256=c9CvChUxBavY20EL4vk9lbP2nY7xaQ0gUYqSZlaQqc0,5249
6
+ thread_recall-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ thread_recall-0.1.0.dist-info/entry_points.txt,sha256=1i4QYU9zhi-BAqc82YBjQrJXhltHS_EixOmihppZWjM,107
8
+ thread_recall-0.1.0.dist-info/licenses/LICENSE,sha256=51_xoiIIUa98JHgVsADHCPu0ReTwVG738gd08erXscM,1076
9
+ thread_recall-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ thread-recall = thread_recall.cli:main
3
+ thread-recall-mcp = thread_recall.mcp_server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pawan Singh Kapkoti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.