chat-memory-sync 0.1.4__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/PKG-INFO +14 -2
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/README.md +13 -1
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/docs/agent-integration.md +1 -1
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/src/chat_memory_sync/__init__.py +1 -1
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/src/chat_memory_sync/cli.py +4 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/src/chat_memory_sync/recall.py +6 -6
- chat_memory_sync-0.2.0/src/chat_memory_sync/web.py +224 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/.gitignore +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/CODE_OF_CONDUCT.md +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/CONTRIBUTING.md +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/LICENSE +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/ROADMAP.md +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/pyproject.toml +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/src/chat_memory_sync/__main__.py +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/src/chat_memory_sync/cmlib.py +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/src/chat_memory_sync/install.py +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/src/chat_memory_sync/run.py +0 -0
- {chat_memory_sync-0.1.4 → chat_memory_sync-0.2.0}/src/chat_memory_sync/sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chat-memory-sync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Unified, searchable memory bank of your AI coding chats across Cursor, Claude Code, OpenCode, Codex, Gemini CLI, Pi and more — synced across machines.
|
|
5
5
|
Project-URL: Homepage, https://github.com/insideaayush/chat-memory-sync
|
|
6
6
|
Project-URL: Repository, https://github.com/insideaayush/chat-memory-sync
|
|
@@ -89,6 +89,7 @@ chat-memory install # config + data repo + daily job + recall
|
|
|
89
89
|
chat-memory sync # pull -> export -> commit -> push (what the job runs)
|
|
90
90
|
chat-memory export # export only (no git)
|
|
91
91
|
chat-memory recall … # search past chats (also the standalone `recall` command)
|
|
92
|
+
chat-memory serve # browse & search your chats in a local web UI
|
|
92
93
|
chat-memory doctor # config, detected harnesses, chat counts
|
|
93
94
|
```
|
|
94
95
|
|
|
@@ -111,7 +112,7 @@ Releases are automated (PyPI + Homebrew) — see [RELEASING.md](RELEASING.md).
|
|
|
111
112
|
cd ~/code/my-project
|
|
112
113
|
recall "auth token refresh" # ranked (BM25) hits for THIS repo, with snippets
|
|
113
114
|
recall "rate limiting" --all # every project
|
|
114
|
-
recall "
|
|
115
|
+
recall "flaky integration test" --harness cursor --since 2026-09 -k 10
|
|
115
116
|
recall "..." --full # just file paths (pipe-friendly)
|
|
116
117
|
```
|
|
117
118
|
|
|
@@ -123,6 +124,17 @@ index.db`), never synced.
|
|
|
123
124
|
hits, then open those `.md` files for full context. Add a line to your project's
|
|
124
125
|
`AGENTS.md`/`CLAUDE.md` so assistants do this automatically.
|
|
125
126
|
|
|
127
|
+
## Browse in a UI
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
chat-memory serve # opens http://127.0.0.1:8787
|
|
131
|
+
chat-memory serve --port 9000 --no-open
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A lightweight local web UI (pure stdlib, no deps, binds to localhost only): a sidebar
|
|
135
|
+
of projects, instant BM25 search across everything, and a reader that renders each
|
|
136
|
+
chat. Backed by the same index as `recall`.
|
|
137
|
+
|
|
126
138
|
## Multi-machine
|
|
127
139
|
|
|
128
140
|
Every machine runs the same job independently. Each run:
|
|
@@ -68,6 +68,7 @@ chat-memory install # config + data repo + daily job + recall
|
|
|
68
68
|
chat-memory sync # pull -> export -> commit -> push (what the job runs)
|
|
69
69
|
chat-memory export # export only (no git)
|
|
70
70
|
chat-memory recall … # search past chats (also the standalone `recall` command)
|
|
71
|
+
chat-memory serve # browse & search your chats in a local web UI
|
|
71
72
|
chat-memory doctor # config, detected harnesses, chat counts
|
|
72
73
|
```
|
|
73
74
|
|
|
@@ -90,7 +91,7 @@ Releases are automated (PyPI + Homebrew) — see [RELEASING.md](RELEASING.md).
|
|
|
90
91
|
cd ~/code/my-project
|
|
91
92
|
recall "auth token refresh" # ranked (BM25) hits for THIS repo, with snippets
|
|
92
93
|
recall "rate limiting" --all # every project
|
|
93
|
-
recall "
|
|
94
|
+
recall "flaky integration test" --harness cursor --since 2026-09 -k 10
|
|
94
95
|
recall "..." --full # just file paths (pipe-friendly)
|
|
95
96
|
```
|
|
96
97
|
|
|
@@ -102,6 +103,17 @@ index.db`), never synced.
|
|
|
102
103
|
hits, then open those `.md` files for full context. Add a line to your project's
|
|
103
104
|
`AGENTS.md`/`CLAUDE.md` so assistants do this automatically.
|
|
104
105
|
|
|
106
|
+
## Browse in a UI
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
chat-memory serve # opens http://127.0.0.1:8787
|
|
110
|
+
chat-memory serve --port 9000 --no-open
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A lightweight local web UI (pure stdlib, no deps, binds to localhost only): a sidebar
|
|
114
|
+
of projects, instant BM25 search across everything, and a reader that renders each
|
|
115
|
+
chat. Backed by the same index as `recall`.
|
|
116
|
+
|
|
105
117
|
## Multi-machine
|
|
106
118
|
|
|
107
119
|
Every machine runs the same job independently. Each run:
|
|
@@ -15,7 +15,7 @@ assistant in Claude Code can read what you discussed in Cursor, Codex, Gemini, e
|
|
|
15
15
|
```bash
|
|
16
16
|
recall "auth token refresh" # ranked hits for the CURRENT project (by CWD)
|
|
17
17
|
recall "rate limiting" --all # across every project
|
|
18
|
-
recall "
|
|
18
|
+
recall "flaky integration test" --harness cursor --since 2026-09 -k 10
|
|
19
19
|
recall "oauth flow" --full # just file paths (easy to pipe / open)
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""chat-memory-sync: unified, searchable memory bank of your AI coding chats."""
|
|
2
|
-
__version__ = "0.
|
|
2
|
+
__version__ = "0.2.0"
|
|
@@ -12,6 +12,7 @@ commands:
|
|
|
12
12
|
sync pull -> export all harnesses -> commit -> push (what the job runs)
|
|
13
13
|
export export only (no git)
|
|
14
14
|
recall search past chats (also available as the `recall` command)
|
|
15
|
+
serve browse & search your chats in a local web UI
|
|
15
16
|
doctor show config, detected harnesses, and chat counts
|
|
16
17
|
version print version
|
|
17
18
|
|
|
@@ -19,6 +20,7 @@ examples:
|
|
|
19
20
|
chat-memory install --data-remote git@github.com:you/chat-memory.git
|
|
20
21
|
chat-memory sync
|
|
21
22
|
chat-memory recall "auth token refresh"
|
|
23
|
+
chat-memory serve
|
|
22
24
|
recall "rate limiting" --all
|
|
23
25
|
"""
|
|
24
26
|
|
|
@@ -51,6 +53,8 @@ def main():
|
|
|
51
53
|
from . import sync; return sync.main()
|
|
52
54
|
if cmd == "recall":
|
|
53
55
|
from . import recall; return recall.main(rest)
|
|
56
|
+
if cmd == "serve":
|
|
57
|
+
from . import web; return web.main(rest)
|
|
54
58
|
if cmd in ("doctor", "status"):
|
|
55
59
|
return doctor()
|
|
56
60
|
print(f"unknown command: {cmd}\n", file=sys.stderr)
|
|
@@ -12,12 +12,12 @@ then open those files for full context.
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
EXAMPLES = """examples:
|
|
15
|
-
recall "
|
|
16
|
-
recall "
|
|
17
|
-
recall "
|
|
18
|
-
recall "
|
|
19
|
-
recall "
|
|
20
|
-
recall --reindex
|
|
15
|
+
recall "rate limiting" auto-scope to the current repo (by CWD)
|
|
16
|
+
recall "retry backoff" --project my-api
|
|
17
|
+
recall "feature flag rollout" --all search every project
|
|
18
|
+
recall "flaky integration test" --harness cursor --since 2026-09 -k 10
|
|
19
|
+
recall "websocket reconnect" --full print file paths only (pipe-friendly)
|
|
20
|
+
recall --reindex force rebuild the index
|
|
21
21
|
"""
|
|
22
22
|
import os, sys, re, glob, sqlite3, argparse, datetime
|
|
23
23
|
from . import cmlib
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
chat-memory serve - a lightweight local web UI to browse & search your chats.
|
|
4
|
+
|
|
5
|
+
Pure standard library (http.server + sqlite). Reuses the recall BM25 index.
|
|
6
|
+
"""
|
|
7
|
+
import os, json, sqlite3, argparse, threading, webbrowser, html
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
|
+
from urllib.parse import urlparse, parse_qs
|
|
10
|
+
from . import cmlib, recall
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _db():
|
|
14
|
+
return sqlite3.connect(cmlib.index_db())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def api_projects():
|
|
18
|
+
recall.ensure_index()
|
|
19
|
+
con = _db(); con.row_factory = sqlite3.Row
|
|
20
|
+
rows = con.execute(
|
|
21
|
+
"SELECT project, COUNT(*) n, MAX(date) last FROM d GROUP BY project ORDER BY last DESC"
|
|
22
|
+
).fetchall()
|
|
23
|
+
con.close()
|
|
24
|
+
total = sum(r["n"] for r in rows)
|
|
25
|
+
return {"total": total, "projects": [dict(r) for r in rows]}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def api_chats(project):
|
|
29
|
+
recall.ensure_index()
|
|
30
|
+
con = _db(); con.row_factory = sqlite3.Row
|
|
31
|
+
rows = con.execute(
|
|
32
|
+
"SELECT date, harness, client, title, path FROM d WHERE project=? ORDER BY date DESC, title",
|
|
33
|
+
(project,)).fetchall()
|
|
34
|
+
con.close()
|
|
35
|
+
return {"chats": [dict(r) for r in rows]}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def api_search(q, project, harness, all_):
|
|
39
|
+
recall.ensure_index()
|
|
40
|
+
rows = recall.search(q, project=None if all_ else (project or None),
|
|
41
|
+
harness=harness or None, k=50)
|
|
42
|
+
out = []
|
|
43
|
+
for r, snip in rows:
|
|
44
|
+
out.append({"date": r["date"], "harness": r["harness"], "client": r["client"],
|
|
45
|
+
"title": r["title"], "path": r["path"], "project": r["project"], "snippet": snip})
|
|
46
|
+
return {"results": out}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def api_chat(path):
|
|
50
|
+
base = os.path.realpath(cmlib.projects_dir())
|
|
51
|
+
full = os.path.realpath(os.path.join(cmlib.data_dir(), path))
|
|
52
|
+
if not full.startswith(base + os.sep) or not full.endswith(".md"):
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
return open(full, encoding="utf-8", errors="ignore").read()
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Handler(BaseHTTPRequestHandler):
|
|
61
|
+
def log_message(self, *a):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def _send(self, body, ctype="application/json", code=200):
|
|
65
|
+
if isinstance(body, (dict, list)):
|
|
66
|
+
body = json.dumps(body)
|
|
67
|
+
data = body.encode("utf-8")
|
|
68
|
+
self.send_response(code)
|
|
69
|
+
self.send_header("Content-Type", ctype)
|
|
70
|
+
self.send_header("Content-Length", str(len(data)))
|
|
71
|
+
self.end_headers()
|
|
72
|
+
self.wfile.write(data)
|
|
73
|
+
|
|
74
|
+
def do_GET(self):
|
|
75
|
+
u = urlparse(self.path)
|
|
76
|
+
q = {k: v[0] for k, v in parse_qs(u.query).items()}
|
|
77
|
+
try:
|
|
78
|
+
if u.path == "/":
|
|
79
|
+
return self._send(INDEX_HTML, "text/html; charset=utf-8")
|
|
80
|
+
if u.path == "/api/projects":
|
|
81
|
+
return self._send(api_projects())
|
|
82
|
+
if u.path == "/api/chats":
|
|
83
|
+
return self._send(api_chats(q.get("project", "")))
|
|
84
|
+
if u.path == "/api/search":
|
|
85
|
+
return self._send(api_search(q.get("q", ""), q.get("project", ""),
|
|
86
|
+
q.get("harness", ""), q.get("all") == "1"))
|
|
87
|
+
if u.path == "/api/chat":
|
|
88
|
+
c = api_chat(q.get("path", ""))
|
|
89
|
+
if c is None:
|
|
90
|
+
return self._send({"error": "not found"}, code=404)
|
|
91
|
+
return self._send({"markdown": c})
|
|
92
|
+
self._send({"error": "not found"}, code=404)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
self._send({"error": str(e)}, code=500)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
INDEX_HTML = r"""<!doctype html><html><head><meta charset="utf-8">
|
|
98
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
99
|
+
<title>chat-memory</title>
|
|
100
|
+
<style>
|
|
101
|
+
:root{--bg:#0f1117;--panel:#171a21;--line:#262b36;--fg:#e6e9ef;--mut:#8b93a7;--acc:#6ea8fe;--hit:#f5d06f}
|
|
102
|
+
*{box-sizing:border-box}body{margin:0;font:14px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--fg);height:100vh;display:flex;flex-direction:column}
|
|
103
|
+
header{display:flex;gap:10px;align-items:center;padding:10px 14px;border-bottom:1px solid var(--line);background:var(--panel)}
|
|
104
|
+
header b{font-weight:700}header .tot{color:var(--mut);font-size:12px}
|
|
105
|
+
input,select{background:#0b0d12;color:var(--fg);border:1px solid var(--line);border-radius:7px;padding:7px 10px;font-size:13px}
|
|
106
|
+
#q{flex:1;min-width:120px}
|
|
107
|
+
main{flex:1;display:flex;min-height:0}
|
|
108
|
+
#side{width:220px;border-right:1px solid var(--line);overflow:auto;background:var(--panel)}
|
|
109
|
+
#list{width:360px;border-right:1px solid var(--line);overflow:auto}
|
|
110
|
+
#view{flex:1;overflow:auto;padding:18px 26px}
|
|
111
|
+
.p{padding:7px 12px;cursor:pointer;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:6px}
|
|
112
|
+
.p:hover,.p.sel{background:#1e2430}.p .n{color:var(--mut);font-size:12px}
|
|
113
|
+
.row{padding:9px 12px;cursor:pointer;border-bottom:1px solid var(--line)}
|
|
114
|
+
.row:hover,.row.sel{background:#1e2430}
|
|
115
|
+
.row .t{font-weight:600;margin-bottom:2px}
|
|
116
|
+
.row .m{color:var(--mut);font-size:11.5px;display:flex;gap:8px;flex-wrap:wrap}
|
|
117
|
+
.row .s{color:var(--mut);font-size:12px;margin-top:4px}
|
|
118
|
+
.tag{background:#222836;border-radius:4px;padding:0 6px;font-size:11px;color:#aab2c5}
|
|
119
|
+
mark{background:transparent;color:var(--hit);font-weight:700}
|
|
120
|
+
#view h1{font-size:19px;margin:0 0 4px}#view h3{margin:18px 0 2px;color:var(--acc);font-size:14px;border-top:1px solid var(--line);padding-top:12px}
|
|
121
|
+
#view pre{background:#0b0d12;border:1px solid var(--line);border-radius:7px;padding:10px;overflow:auto;font-size:12.5px}
|
|
122
|
+
#view code{font-family:ui-monospace,Menlo,monospace}
|
|
123
|
+
#view details{margin:6px 0;color:var(--mut)}#view summary{cursor:pointer}
|
|
124
|
+
.meta{color:var(--mut);font-size:12px;margin-bottom:14px}
|
|
125
|
+
.empty{color:var(--mut);padding:24px;text-align:center}
|
|
126
|
+
</style></head><body>
|
|
127
|
+
<header><b>chat-memory</b><span class="tot" id="tot"></span>
|
|
128
|
+
<input id="q" placeholder="search all your chats…" autocomplete="off">
|
|
129
|
+
<label class="tot"><input type="checkbox" id="allp"> all projects</label>
|
|
130
|
+
<select id="harness"><option value="">any harness</option></select>
|
|
131
|
+
</header>
|
|
132
|
+
<main>
|
|
133
|
+
<div id="side"></div><div id="list"></div><div id="view"><div class="empty">Pick a project or search.</div></div>
|
|
134
|
+
</main>
|
|
135
|
+
<script>
|
|
136
|
+
const E=(t,c,h)=>{const e=document.createElement(t);if(c)e.className=c;if(h!=null)e.innerHTML=h;return e};
|
|
137
|
+
const esc=s=>(s||'').replace(/[&<>]/g,m=>({'&':'&','<':'<','>':'>'}[m]));
|
|
138
|
+
let state={project:null,harness:''};
|
|
139
|
+
const HARN=['cursor','cursor-agent','claude-code','opencode','pi','codex','gemini'];
|
|
140
|
+
HARN.forEach(h=>harness.appendChild(E('option',null,h)).value=h);
|
|
141
|
+
harness.onchange=()=>{state.harness=harness.value; q.value?doSearch():loadChats()};
|
|
142
|
+
|
|
143
|
+
async function j(u){return (await fetch(u)).json()}
|
|
144
|
+
async function loadProjects(){
|
|
145
|
+
const d=await j('/api/projects'); tot.textContent=d.total+' chats';
|
|
146
|
+
side.innerHTML='';
|
|
147
|
+
d.projects.forEach(p=>{const el=E('div','p');el.append(E('span',null,esc(p.project)),E('span','n',p.n));
|
|
148
|
+
el.onclick=()=>{document.querySelectorAll('.p').forEach(x=>x.classList.remove('sel'));el.classList.add('sel');state.project=p.project;q.value='';loadChats()};side.append(el)});
|
|
149
|
+
}
|
|
150
|
+
function hl(s){return esc(s).replace(/>>(.*?)<</g,'<mark>$1</mark>')}
|
|
151
|
+
function rowEl(r,withSnip){
|
|
152
|
+
const el=E('div','row');
|
|
153
|
+
el.append(E('div','t',esc(r.title)||'(untitled)'));
|
|
154
|
+
const m=E('div','m');m.append(E('span','tag',r.harness),E('span',null,r.date||''),E('span',null,esc(r.client||'')));
|
|
155
|
+
if(withSnip&&r.project)m.append(E('span','tag',esc(r.project)));
|
|
156
|
+
el.append(m);
|
|
157
|
+
if(withSnip&&r.snippet)el.append(E('div','s',hl(r.snippet)));
|
|
158
|
+
el.onclick=()=>{document.querySelectorAll('.row').forEach(x=>x.classList.remove('sel'));el.classList.add('sel');openChat(r.path)};
|
|
159
|
+
return el;
|
|
160
|
+
}
|
|
161
|
+
async function loadChats(){
|
|
162
|
+
if(!state.project){list.innerHTML='';return}
|
|
163
|
+
const d=await j('/api/chats?project='+encodeURIComponent(state.project));
|
|
164
|
+
list.innerHTML=''; if(!d.chats.length){list.append(E('div','empty','No chats.'));return}
|
|
165
|
+
d.chats.filter(c=>!state.harness||c.harness===state.harness).forEach(c=>list.append(rowEl(c,false)));
|
|
166
|
+
}
|
|
167
|
+
let tmr;
|
|
168
|
+
q.oninput=()=>{clearTimeout(tmr);tmr=setTimeout(doSearch,180)};
|
|
169
|
+
async function doSearch(){
|
|
170
|
+
const term=q.value.trim(); if(!term)return loadChats();
|
|
171
|
+
const u='/api/search?q='+encodeURIComponent(term)+'&all='+(allp.checked?1:0)+
|
|
172
|
+
(state.project&&!allp.checked?'&project='+encodeURIComponent(state.project):'')+
|
|
173
|
+
(state.harness?'&harness='+state.harness:'');
|
|
174
|
+
const d=await j(u); list.innerHTML='';
|
|
175
|
+
if(!d.results.length){list.append(E('div','empty','No matches.'));return}
|
|
176
|
+
d.results.forEach(r=>list.append(rowEl(r,true)));
|
|
177
|
+
}
|
|
178
|
+
allp.onchange=doSearch;
|
|
179
|
+
function md(src){
|
|
180
|
+
// split frontmatter
|
|
181
|
+
let meta='';if(src.startsWith('---')){const e=src.indexOf('\n---',3);if(e>-1){meta=src.slice(3,e).trim();src=src.slice(e+4)}}
|
|
182
|
+
let h=esc(src);
|
|
183
|
+
h=h.replace(/```([\w-]*)\n([\s\S]*?)```/g,(m,l,c)=>'<pre><code>'+c.replace(/&/g,'&')+'</code></pre>');
|
|
184
|
+
h=h.replace(/<details><summary>([\s\S]*?)<\/summary>/g,'<details><summary>$1</summary>').replace(/<\/details>/g,'</details>');
|
|
185
|
+
h=h.replace(/^### (.*)$/gm,'<h3>$1</h3>').replace(/^## (.*)$/gm,'<h3>$1</h3>').replace(/^# (.*)$/gm,'<h1>$1</h1>');
|
|
186
|
+
h=h.replace(/\*\*(.+?)\*\*/g,'<b>$1</b>');
|
|
187
|
+
h=h.replace(/`([^`\n]+?)`/g,'<code>$1</code>');
|
|
188
|
+
const mh=meta?'<div class="meta">'+esc(meta).replace(/\n/g,' · ')+'</div>':'';
|
|
189
|
+
return mh+h;
|
|
190
|
+
}
|
|
191
|
+
async function openChat(path){
|
|
192
|
+
view.innerHTML='<div class="empty">loading…</div>';
|
|
193
|
+
const d=await j('/api/chat?path='+encodeURIComponent(path));
|
|
194
|
+
view.innerHTML=d.markdown?md(d.markdown):'<div class="empty">not found</div>';
|
|
195
|
+
view.scrollTop=0;
|
|
196
|
+
}
|
|
197
|
+
loadProjects();
|
|
198
|
+
</script></body></html>"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def serve(port=8787, open_browser=True):
|
|
202
|
+
recall.ensure_index()
|
|
203
|
+
httpd = ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
|
204
|
+
url = f"http://127.0.0.1:{port}"
|
|
205
|
+
print(f"chat-memory UI -> {url} (Ctrl-C to stop)")
|
|
206
|
+
if open_browser:
|
|
207
|
+
threading.Timer(0.6, lambda: webbrowser.open(url)).start()
|
|
208
|
+
try:
|
|
209
|
+
httpd.serve_forever()
|
|
210
|
+
except KeyboardInterrupt:
|
|
211
|
+
print("\nstopped")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main(argv=None):
|
|
215
|
+
ap = argparse.ArgumentParser(prog="chat-memory serve",
|
|
216
|
+
description="browse & search your chats in a local web UI")
|
|
217
|
+
ap.add_argument("--port", type=int, default=8787)
|
|
218
|
+
ap.add_argument("--no-open", action="store_true", help="don't auto-open the browser")
|
|
219
|
+
args = ap.parse_args(argv)
|
|
220
|
+
serve(args.port, open_browser=not args.no_open)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if __name__ == "__main__":
|
|
224
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|