memex-mvp 0.5.4 → 0.6.1
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.
- package/HELP.md +48 -0
- package/README.md +19 -0
- package/README.ru.md +20 -3
- package/lib/store-doc/canonicalize.js +116 -0
- package/lib/store-doc/detect.js +209 -0
- package/lib/store-doc/extract-title.js +116 -0
- package/package.json +2 -2
- package/server.js +318 -0
- package/skills/install-memex/README.md +86 -38
- package/skills/install-memex/SKILL.md +11 -1
- package/skills/install-memex/examples.md +218 -0
package/HELP.md
CHANGED
|
@@ -227,12 +227,60 @@ Memex по дефолту сортирует по **релевантности**
|
|
|
227
227
|
|
|
228
228
|
---
|
|
229
229
|
|
|
230
|
+
### 8. 🔗 Сохранение URL'ов в memex (Perplexity, статьи, AI-share'ы)
|
|
231
|
+
|
|
232
|
+
Ты читаешь что-то — Perplexity research thread, длинную статью, GitHub-обсуждение, AI-chat share — и хочешь чтобы это жило в memex-памяти, искалось из любого AI-чата.
|
|
233
|
+
|
|
234
|
+
**В любом MCP-агенте (Claude Code, Cursor, Cline, Continue, Zed):**
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
Сохрани https://perplexity.ai/share/<id> в memex
|
|
238
|
+
Добавь эту статью в memex: https://example.com/great-post
|
|
239
|
+
Захвати этот ChatGPT-разговор: https://chat.openai.com/share/<id>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Что происходит за кулисами:**
|
|
243
|
+
|
|
244
|
+
1. Агент сам делает fetch URL'a (через свой WebFetch)
|
|
245
|
+
2. Если страница защищена Cloudflare (Perplexity, npm.com, Twitter, Medium…) — агент авто-retry через `r.jina.ai` proxy (бесплатный JS-runtime, обходит Cloudflare)
|
|
246
|
+
3. Агент вызывает `memex_store_document(content, url, title)`
|
|
247
|
+
4. Memex сохраняет содержимое как conversation с `source: "web"` — ищется через `memex_search` рядом с AI-чатами
|
|
248
|
+
|
|
249
|
+
**Для Perplexity-thread'ов:** thread должен быть **PUBLIC**. В Perplexity: открой thread → Share → toggle "Public link" → скопируй новый URL → дай его агенту. URL из адресной строки браузера (`perplexity.ai/search/<id>`) — это **твой owner-URL, не shareable**.
|
|
250
|
+
|
|
251
|
+
Если забудешь — memex детектит «private» в ответе и agent тебе явно скажет что делать.
|
|
252
|
+
|
|
253
|
+
**Login-walled или paywalled контент не fetch'нется** (NYT subscription, твои приватные ChatGPT-чаты). Для них вставь контент руками:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
Сохрани этот текст в memex (название: "..."): <вставь содержимое>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Tag'и при сохранении** — для последующей фильтрации:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
Сохрани https://... в memex, поставь теги "research" и "perplexity"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Поиск:**
|
|
266
|
+
|
|
267
|
+
```
|
|
268
|
+
Найди в memex что Perplexity говорил про X на прошлой неделе
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`memex_search` возвращает совпадения **и из AI-чатов, и из сохранённых URL'ов** в одном запросе — отсортировано по релевантности или дате.
|
|
272
|
+
|
|
273
|
+
**Memex принципиально НЕ делает outbound network calls.** Fetcher живёт в твоём AI-агенте. Если он использует Jina для обхода Cloudflare — Jina видит URL (но НЕ остальной memex-корпус). Это выбор агента, не memex'a.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
230
277
|
## Какие MCP-tools агент может вызвать
|
|
231
278
|
|
|
232
279
|
| Tool | Что делает |
|
|
233
280
|
|---|---|
|
|
234
281
|
| `memex_overview` | Снэпшот корпуса: источники, сколько сообщений, последние чаты, статус auto-capture |
|
|
235
282
|
| `memex_search(query)` | Полнотекстовый поиск (FTS5) с recency boost'ом. Параметры: `project`, `source`, `chat`, `half_life_days`, `expand_match`, `sort` |
|
|
283
|
+
| `memex_store_document(content, url?, title?)` | Сохранить внешний документ (web-страница, AI-chat share, paste) в memex. Агент сам делает fetch, memex хранит verbatim. Учит Jina-трюк для Cloudflare-страниц |
|
|
236
284
|
| `memex_list_projects` | Список всех проектов с количеством разговоров |
|
|
237
285
|
| `memex_list_conversations` | Список чатов отсортированных по recency |
|
|
238
286
|
| `memex_get_conversation(id)` | Полный transcript одного чата |
|
package/README.md
CHANGED
|
@@ -100,6 +100,23 @@ For a fully-automated install across all detected MCP clients, see [the AI-drive
|
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
103
|
+
## Save URLs into memex (v0.6+)
|
|
104
|
+
|
|
105
|
+
Once memex is installed, any MCP-aware agent can also save **web pages, AI chat shares, and pasted text** into your memex memory — searchable from any other AI chat later. In Claude Code, Cursor, Cline, …:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
Save https://www.perplexity.ai/share/<id> to memex
|
|
109
|
+
Add this article to my memex: https://example.com/long-post
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The agent fetches the page via its own WebFetch (auto-falling back to `r.jina.ai` for Cloudflare-protected sites — memex teaches the trick) and calls `memex_store_document`. Memex stores the content verbatim as a `web` source conversation, indistinguishable from AI chats at search time.
|
|
113
|
+
|
|
114
|
+
Perplexity threads need to be made **Public** in the Share dialog first — memex detects private threads and tells the user how to fix it. Full guide: [HELP.md §8](HELP.md).
|
|
115
|
+
|
|
116
|
+
**Memex stays 100% local** — the agent fetches, memex only stores. Zero outbound calls from memex itself.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
103
120
|
## What it captures
|
|
104
121
|
|
|
105
122
|
| Source | How it gets in |
|
|
@@ -111,6 +128,7 @@ For a fully-automated install across all detected MCP clients, see [the AI-drive
|
|
|
111
128
|
| Obsidian notes | Auto: per-vault markdown watcher |
|
|
112
129
|
| Telegram exports | Manual: drop `result.json` (Telegram Desktop) into `~/.memex/inbox/` |
|
|
113
130
|
| Telegram (live) | Run [`memex-bot`](bot/README.md) — captures messages you send/forward to your private bot |
|
|
131
|
+
| **Web pages, AI chat shares, pasted text** | From any MCP agent: *"save https://... to memex"*. Agent fetches; memex stores verbatim. Cloudflare-protected pages (Perplexity, npm.com, Twitter, Medium, …) handled via the agent's r.jina.ai fallback. See [HELP.md §8](HELP.md) |
|
|
114
132
|
|
|
115
133
|
All sources land in the same FTS5 corpus, searchable by one `memex_search` call.
|
|
116
134
|
|
|
@@ -128,6 +146,7 @@ All sources land in the same FTS5 corpus, searchable by one `memex_search` call.
|
|
|
128
146
|
| `memex_list_projects` | Distinct project paths captured (for the `project` filter) |
|
|
129
147
|
| `memex_archive_conversation` | Hide a chat from default listings (data preserved) |
|
|
130
148
|
| `memex_export_markdown` | Export one conversation as Markdown (for Obsidian round-trip) |
|
|
149
|
+
| `memex_store_document` | Save a web page, AI chat share, or pasted text. Agent fetches; memex stores verbatim. Teaches the Jina r.jina.ai trick for Cloudflare-blocked pages |
|
|
131
150
|
| `memex_list_sources` | Per-source enabled/disabled + counts |
|
|
132
151
|
| `memex_status` | Daemon health: PID, last capture, watched files |
|
|
133
152
|
| `memex_sources_status` | Which sources are captured + the exact CLI to opt out |
|
package/README.ru.md
CHANGED
|
@@ -121,6 +121,21 @@ curl -fsSL https://raw.githubusercontent.com/parallelclaw/memex-mvp/main/skills/
|
|
|
121
121
|
|
|
122
122
|
…или `/install-memex`. Агент сам сделает `npm install`, пропишет MCP-config, поднимет daemon и проверит что всё работает — ~2 минуты.
|
|
123
123
|
|
|
124
|
+
### Сохранение URL'ов в memex (v0.6+)
|
|
125
|
+
|
|
126
|
+
После установки в любом MCP-агенте (Claude Code, Cursor, Cline, Continue, Zed) можно сохранять **web-страницы, AI-chat share'ы и pasted-тексты** прямо в memex-память:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Сохрани https://www.perplexity.ai/share/<id> в memex
|
|
130
|
+
Добавь эту статью в memex: https://example.com/article
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Агент сам fetch'ит страницу через свой WebFetch — для Cloudflare-защищённых сайтов (Perplexity, npm.com, Twitter, Medium) автоматически falls back на `r.jina.ai` proxy (memex учит агента этому трюку через tool description). Затем агент вызывает `memex_store_document`, который хранит контент verbatim как conversation с `source: "web"`.
|
|
134
|
+
|
|
135
|
+
**Memex остаётся 100% локальным** — fetch делает агент, memex только хранит. Никаких outbound network calls со стороны memex.
|
|
136
|
+
|
|
137
|
+
Полное руководство и edge cases (private Perplexity, paywall, login-walls): [HELP.md §8](HELP.md).
|
|
138
|
+
|
|
124
139
|
### Подключение к Claude Code
|
|
125
140
|
|
|
126
141
|
Сначала возьми **два абсолютных пути** в терминале:
|
|
@@ -162,9 +177,11 @@ which node # → путь до бинарника node (например /Users
|
|
|
162
177
|
| **Cursor IDE** (Composer + Chat) | SQLite `state.vscdb` в `~/Library/Application Support/Cursor/` | ✅ работает (poll каждые 5 мин) |
|
|
163
178
|
| **Obsidian** vault notes | `.md` файлы + YAML frontmatter | ✅ работает (FSEvents, hash-based dedupe) |
|
|
164
179
|
| **Telegram** | `result.json` из Desktop export | ✅ работает |
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
|
180
|
+
| **Telegram (live)** | бот `memex-bot` ловит твои сообщения / форварды | ✅ работает |
|
|
181
|
+
| **Web-страницы, AI-share'ы, paste'ы** | `memex_store_document` — агент fetch'ит, memex хранит verbatim (v0.6+) | ✅ работает |
|
|
182
|
+
| Claude.ai web export | будет в v0.7 | — |
|
|
183
|
+
| ChatGPT export | будет в v0.7 | — |
|
|
184
|
+
| Apple Notes | будет в v0.7 | — |
|
|
168
185
|
|
|
169
186
|
### Filename convention для inbox-файлов
|
|
170
187
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL canonicalization for stable deduplication of stored web documents.
|
|
3
|
+
*
|
|
4
|
+
* Goal: two URLs that point to "the same document" should map to the same
|
|
5
|
+
* canonical form, so memex_store_document gives them the same conversation_id
|
|
6
|
+
* via sha256(canonical).
|
|
7
|
+
*
|
|
8
|
+
* What we normalize:
|
|
9
|
+
* - Lowercase scheme + host
|
|
10
|
+
* - Strip known tracking params (utm_*, fbclid, gclid, ref, mc_*, _ga, …)
|
|
11
|
+
* - Drop the fragment (#anchor) — same document
|
|
12
|
+
* - Normalize trailing slash on pathname
|
|
13
|
+
*
|
|
14
|
+
* What we DON'T normalize:
|
|
15
|
+
* - Path case (some servers are case-sensitive)
|
|
16
|
+
* - Non-tracking query params (?q= search, ?id= permalinks — meaningful)
|
|
17
|
+
* - Port (rare in public URLs)
|
|
18
|
+
*
|
|
19
|
+
* If the input isn't a valid URL, we return the input unchanged. Callers
|
|
20
|
+
* should still hash the result for deduplication.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Well-known tracking-param families. Case-insensitive prefix match.
|
|
24
|
+
const TRACKING_PREFIXES = [
|
|
25
|
+
'utm_', // Google Analytics
|
|
26
|
+
'mc_', // Mailchimp
|
|
27
|
+
];
|
|
28
|
+
const TRACKING_EXACT = new Set([
|
|
29
|
+
'fbclid', // Facebook
|
|
30
|
+
'gclid', // Google ads
|
|
31
|
+
'dclid', // Google DoubleClick
|
|
32
|
+
'gbraid', // Google
|
|
33
|
+
'wbraid', // Google
|
|
34
|
+
'yclid', // Yandex
|
|
35
|
+
'msclkid', // Microsoft ads
|
|
36
|
+
'twclid', // Twitter
|
|
37
|
+
'igshid', // Instagram
|
|
38
|
+
'ref', // generic referrer
|
|
39
|
+
'ref_source',
|
|
40
|
+
'ref_url',
|
|
41
|
+
'referrer',
|
|
42
|
+
'source', // common referrer flag (NOT always tracking but very often)
|
|
43
|
+
'_ga', // Google Analytics
|
|
44
|
+
'_gl', // Google Analytics linker
|
|
45
|
+
'hsCtaTracking',
|
|
46
|
+
'hsenc',
|
|
47
|
+
'hsmi',
|
|
48
|
+
'mkt_tok',
|
|
49
|
+
'pk_campaign',
|
|
50
|
+
'pk_source',
|
|
51
|
+
'pk_medium',
|
|
52
|
+
'pk_keyword',
|
|
53
|
+
'pk_content',
|
|
54
|
+
'vero_id',
|
|
55
|
+
'vero_conv',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function isTrackingParam(name) {
|
|
59
|
+
const lower = name.toLowerCase();
|
|
60
|
+
if (TRACKING_EXACT.has(lower)) return true;
|
|
61
|
+
for (const prefix of TRACKING_PREFIXES) {
|
|
62
|
+
if (lower.startsWith(prefix)) return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} rawUrl
|
|
69
|
+
* @returns {string} canonicalized URL (or the input unchanged if unparseable)
|
|
70
|
+
*/
|
|
71
|
+
export function canonicalize(rawUrl) {
|
|
72
|
+
if (typeof rawUrl !== 'string' || !rawUrl.trim()) return rawUrl;
|
|
73
|
+
|
|
74
|
+
let u;
|
|
75
|
+
try {
|
|
76
|
+
u = new URL(rawUrl.trim());
|
|
77
|
+
} catch (_) {
|
|
78
|
+
return rawUrl.trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Lowercase scheme + host (URL parser already does that, but be explicit)
|
|
82
|
+
u.protocol = u.protocol.toLowerCase();
|
|
83
|
+
u.hostname = u.hostname.toLowerCase();
|
|
84
|
+
|
|
85
|
+
// Drop the fragment
|
|
86
|
+
u.hash = '';
|
|
87
|
+
|
|
88
|
+
// Strip tracking params
|
|
89
|
+
const cleanParams = new URLSearchParams();
|
|
90
|
+
for (const [k, v] of u.searchParams) {
|
|
91
|
+
if (!isTrackingParam(k)) cleanParams.append(k, v);
|
|
92
|
+
}
|
|
93
|
+
u.search = cleanParams.toString();
|
|
94
|
+
|
|
95
|
+
// Normalize trailing slash: drop trailing slash on non-root paths,
|
|
96
|
+
// so /foo and /foo/ are treated as the same document
|
|
97
|
+
if (u.pathname.length > 1 && u.pathname.endsWith('/')) {
|
|
98
|
+
u.pathname = u.pathname.replace(/\/+$/, '');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return u.toString();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Best-effort domain extraction for metadata (e.g. "perplexity.ai").
|
|
106
|
+
* Returns null for unparseable URLs.
|
|
107
|
+
*/
|
|
108
|
+
export function extractDomain(rawUrl) {
|
|
109
|
+
if (typeof rawUrl !== 'string') return null;
|
|
110
|
+
try {
|
|
111
|
+
const u = new URL(rawUrl);
|
|
112
|
+
return u.hostname.toLowerCase().replace(/^www\./, '');
|
|
113
|
+
} catch (_) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern detection for memex_store_document.
|
|
3
|
+
*
|
|
4
|
+
* When the agent passes content to memex_store_document, memex sniffs it
|
|
5
|
+
* for known failure signatures (Cloudflare challenge, Perplexity-private,
|
|
6
|
+
* paywalls, …) and returns actionable warnings.
|
|
7
|
+
*
|
|
8
|
+
* Each detector returns either null or an object:
|
|
9
|
+
* { type, blocking, message }
|
|
10
|
+
*
|
|
11
|
+
* `blocking: true` → memex returns stored:false to the agent. Use only for
|
|
12
|
+
* clear-cut failures where storing the content would pollute the corpus.
|
|
13
|
+
* `blocking: false` → memex stores the content but appends the warning so
|
|
14
|
+
* the agent can decide whether to surface it to the user.
|
|
15
|
+
*
|
|
16
|
+
* Patterns may grow over time as new failure modes appear in real use.
|
|
17
|
+
* Single-purpose regexes — order matters (more specific first).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const CLOUDFLARE_PATTERNS = [
|
|
21
|
+
/Just a moment\.\.\./i,
|
|
22
|
+
/Verifying you are human/i,
|
|
23
|
+
/cf-turnstile/i,
|
|
24
|
+
/cf_chl_opt/i,
|
|
25
|
+
/__cf_bm/i, // Cloudflare's bot-management cookie name in challenge pages
|
|
26
|
+
/Checking your browser before accessing/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const PERPLEXITY_PRIVATE_PATTERNS = [
|
|
30
|
+
/This thread is private/i,
|
|
31
|
+
/Sign in if you are the owner of this thread/i,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const LOGIN_REQUIRED_PATTERNS = [
|
|
35
|
+
/Sign in to continue/i,
|
|
36
|
+
/Please log in to continue/i,
|
|
37
|
+
/Login required/i,
|
|
38
|
+
/<form[^>]+action=["']\/?(login|signin|sign-in|auth)/i,
|
|
39
|
+
/You must be logged in/i,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const PAYWALL_PATTERNS = [
|
|
43
|
+
/Subscribe to (read|continue)/i,
|
|
44
|
+
/Continue reading with subscription/i,
|
|
45
|
+
/\bpaywall\b/i,
|
|
46
|
+
/You've reached your free article limit/i,
|
|
47
|
+
/metered access/i,
|
|
48
|
+
/Become a (member|subscriber)/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Detect a Cloudflare bot-challenge page. Blocking.
|
|
53
|
+
*/
|
|
54
|
+
function detectCloudflareChallenge(content, _url) {
|
|
55
|
+
for (const re of CLOUDFLARE_PATTERNS) {
|
|
56
|
+
if (re.test(content)) {
|
|
57
|
+
return {
|
|
58
|
+
type: 'cloudflare-challenge',
|
|
59
|
+
blocking: true,
|
|
60
|
+
message:
|
|
61
|
+
'The content you passed looks like a Cloudflare bot-challenge page, not the actual content. The page is protected by Cloudflare and your direct fetch was blocked.\n\n' +
|
|
62
|
+
'Retry by prepending https://r.jina.ai/ to the URL:\n\n' +
|
|
63
|
+
' WebFetch("https://r.jina.ai/" + originalUrl)\n\n' +
|
|
64
|
+
'Jina AI Reader runs a real browser server-side and bypasses Cloudflare. Returns clean markdown. Free for personal use, no API key needed.\n\n' +
|
|
65
|
+
'If Jina ALSO fails or returns "private/login required", the page needs authentication — Jina can\'t bypass that.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detect a private Perplexity thread. Blocking — Jina can't help here,
|
|
74
|
+
* the user needs to make the thread public first.
|
|
75
|
+
*/
|
|
76
|
+
function detectPerplexityPrivate(content, url) {
|
|
77
|
+
// Only flag if we have a URL hint that it's Perplexity, OR if the message
|
|
78
|
+
// text is unambiguously Perplexity's phrasing.
|
|
79
|
+
const isPerplexityUrl =
|
|
80
|
+
typeof url === 'string' && /perplexity\.ai/i.test(url);
|
|
81
|
+
|
|
82
|
+
let matched = false;
|
|
83
|
+
for (const re of PERPLEXITY_PRIVATE_PATTERNS) {
|
|
84
|
+
if (re.test(content)) {
|
|
85
|
+
matched = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!matched) return null;
|
|
90
|
+
if (!isPerplexityUrl && !/perplexity/i.test(content)) {
|
|
91
|
+
// Same phrasing might appear on other sites — only act if we're confident
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
type: 'perplexity-private',
|
|
97
|
+
blocking: true,
|
|
98
|
+
message:
|
|
99
|
+
'This Perplexity thread is marked private — even Jina Reader can\'t access it (this is an authentication wall, not Cloudflare bot protection).\n\n' +
|
|
100
|
+
'Tell the user: "To save this Perplexity thread to memex, you need to make it public first:\n' +
|
|
101
|
+
' 1. Open the thread in Perplexity\n' +
|
|
102
|
+
' 2. Click Share (top right)\n' +
|
|
103
|
+
' 3. Toggle \'Public link\' on\n' +
|
|
104
|
+
' 4. Copy the new shareable URL Perplexity shows\n' +
|
|
105
|
+
' 5. Send me THAT URL — it\'ll work"\n\n' +
|
|
106
|
+
'The URL in the user\'s address bar (perplexity.ai/search/<id>) is the owner\'s private URL, not the shareable one.',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Suspiciously short content from a URL that should be substantive.
|
|
112
|
+
* Non-blocking — we store it, but warn.
|
|
113
|
+
*/
|
|
114
|
+
function detectSuspiciouslySmall(content, url) {
|
|
115
|
+
const trimmed = (content || '').trim();
|
|
116
|
+
// Threshold: documents shorter than 200 chars are almost certainly noise
|
|
117
|
+
// (error pages, redirects, JS-only stubs). Pasted snippets can legitimately
|
|
118
|
+
// be that short, so only flag when we have a URL (suggesting a fetch was
|
|
119
|
+
// attempted) — pastes get a free pass.
|
|
120
|
+
if (!url) return null;
|
|
121
|
+
if (trimmed.length >= 200) return null;
|
|
122
|
+
return {
|
|
123
|
+
type: 'suspiciously-small',
|
|
124
|
+
blocking: false,
|
|
125
|
+
message:
|
|
126
|
+
`The content you passed is very short (${trimmed.length} chars). ` +
|
|
127
|
+
'The page might have been blocked, redirect-failed, or be JS-rendered with no SSR. ' +
|
|
128
|
+
'Stored as-is — consider verifying with the user that this is what they expected.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Login required (form / prompt). Non-blocking but worth flagging.
|
|
134
|
+
*/
|
|
135
|
+
function detectLoginRequired(content, _url) {
|
|
136
|
+
for (const re of LOGIN_REQUIRED_PATTERNS) {
|
|
137
|
+
if (re.test(content)) {
|
|
138
|
+
return {
|
|
139
|
+
type: 'login-required',
|
|
140
|
+
blocking: false,
|
|
141
|
+
message:
|
|
142
|
+
'The page appears to require login (sign-in prompt / login form detected). ' +
|
|
143
|
+
'The content you stored may be a login page, not the actual content the user wanted. ' +
|
|
144
|
+
'Ask the user to paste the content manually if this isn\'t what they expected.',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Paywall / subscription-gated content. Non-blocking.
|
|
153
|
+
*/
|
|
154
|
+
function detectPaywalled(content, _url) {
|
|
155
|
+
for (const re of PAYWALL_PATTERNS) {
|
|
156
|
+
if (re.test(content)) {
|
|
157
|
+
return {
|
|
158
|
+
type: 'paywalled',
|
|
159
|
+
blocking: false,
|
|
160
|
+
message:
|
|
161
|
+
'The page appears to be paywalled (subscription/payment prompt detected). ' +
|
|
162
|
+
'The content stored may just be the teaser. ' +
|
|
163
|
+
'If the user has full access, they can paste the complete article manually.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns array of warnings sorted with blocking warnings first.
|
|
172
|
+
* If the first warning is blocking, memex should refuse the store
|
|
173
|
+
* and return that warning to the agent.
|
|
174
|
+
*
|
|
175
|
+
* Detectors run in this order (more-specific first):
|
|
176
|
+
* 1. cloudflare-challenge (blocking)
|
|
177
|
+
* 2. perplexity-private (blocking)
|
|
178
|
+
* 3. suspiciously-small (non-blocking)
|
|
179
|
+
* 4. login-required (non-blocking)
|
|
180
|
+
* 5. paywalled (non-blocking)
|
|
181
|
+
*/
|
|
182
|
+
export function detectIssues(content, url) {
|
|
183
|
+
const safeContent = typeof content === 'string' ? content : '';
|
|
184
|
+
const warnings = [];
|
|
185
|
+
|
|
186
|
+
// Blocking first — stop on first hit so we surface the most actionable.
|
|
187
|
+
const blocking =
|
|
188
|
+
detectCloudflareChallenge(safeContent, url) ||
|
|
189
|
+
detectPerplexityPrivate(safeContent, url);
|
|
190
|
+
if (blocking) {
|
|
191
|
+
warnings.push(blocking);
|
|
192
|
+
return warnings;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Non-blocking — collect all that match.
|
|
196
|
+
for (const fn of [detectSuspiciouslySmall, detectLoginRequired, detectPaywalled]) {
|
|
197
|
+
const w = fn(safeContent, url);
|
|
198
|
+
if (w) warnings.push(w);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return warnings;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Convenience: is any warning blocking?
|
|
206
|
+
*/
|
|
207
|
+
export function isBlocked(warnings) {
|
|
208
|
+
return Array.isArray(warnings) && warnings.some((w) => w.blocking);
|
|
209
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a title from fetched page content.
|
|
3
|
+
*
|
|
4
|
+
* Strategy (first hit wins):
|
|
5
|
+
* 1. Markdown H1 — `# Title text` (Jina Reader's output starts with this)
|
|
6
|
+
* 2. HTML <title> — `<title>Page Title</title>`
|
|
7
|
+
* 3. HTML <h1> — `<h1>Page Title</h1>`
|
|
8
|
+
* 4. First non-empty line if short enough to look like a title
|
|
9
|
+
* 5. URL slug fallback — last meaningful path segment, decoded
|
|
10
|
+
* 6. Domain fallback — just the domain name
|
|
11
|
+
* 7. "Untitled document"
|
|
12
|
+
*
|
|
13
|
+
* Returns a trimmed string up to MAX_LEN characters. Always returns a
|
|
14
|
+
* non-empty string (worst case "Untitled document").
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MAX_LEN = 200;
|
|
18
|
+
|
|
19
|
+
function trimTitle(s) {
|
|
20
|
+
if (!s) return '';
|
|
21
|
+
let t = String(s).replace(/\s+/g, ' ').trim();
|
|
22
|
+
if (t.length > MAX_LEN) t = t.slice(0, MAX_LEN).trim() + '…';
|
|
23
|
+
return t;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fromMarkdownH1(content) {
|
|
27
|
+
// Markdown H1: line starts with single # then space, then text.
|
|
28
|
+
// Use \r? for cross-platform line endings. Stop at end-of-line.
|
|
29
|
+
const m = content.match(/^[ \t]*#[ \t]+([^\r\n]+?)[ \t]*$/m);
|
|
30
|
+
return m ? trimTitle(m[1]) : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fromHtmlTitle(content) {
|
|
34
|
+
const m = content.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
35
|
+
return m ? trimTitle(decodeEntities(m[1])) : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fromHtmlH1(content) {
|
|
39
|
+
// Inner text only — strip nested tags like <span>...</span>
|
|
40
|
+
const m = content.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
|
|
41
|
+
if (!m) return '';
|
|
42
|
+
const inner = m[1].replace(/<[^>]+>/g, '');
|
|
43
|
+
return trimTitle(decodeEntities(inner));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fromFirstLine(content) {
|
|
47
|
+
// First non-empty line, but only if it looks like a heading
|
|
48
|
+
// (short-ish, no markdown junk).
|
|
49
|
+
const lines = content.split(/\r?\n/);
|
|
50
|
+
for (const raw of lines) {
|
|
51
|
+
const line = raw.trim();
|
|
52
|
+
if (!line) continue;
|
|
53
|
+
// Skip leading markdown decorators / metadata
|
|
54
|
+
if (/^[#\-=*>|`]/.test(line)) continue;
|
|
55
|
+
if (line.length > 0 && line.length <= 120) {
|
|
56
|
+
return trimTitle(line);
|
|
57
|
+
}
|
|
58
|
+
// First substantive line is too long — give up on this strategy
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fromUrlSlug(rawUrl) {
|
|
65
|
+
if (!rawUrl) return '';
|
|
66
|
+
try {
|
|
67
|
+
const u = new URL(rawUrl);
|
|
68
|
+
// Last meaningful path segment
|
|
69
|
+
const segs = u.pathname.split('/').filter(Boolean);
|
|
70
|
+
if (segs.length) {
|
|
71
|
+
const slug = decodeURIComponent(segs[segs.length - 1])
|
|
72
|
+
.replace(/[-_]+/g, ' ')
|
|
73
|
+
.replace(/\.(html?|md|pdf|txt)$/i, '')
|
|
74
|
+
.trim();
|
|
75
|
+
if (slug) return trimTitle(slug);
|
|
76
|
+
}
|
|
77
|
+
// No useful path — fall through to domain
|
|
78
|
+
return trimTitle(u.hostname.replace(/^www\./, ''));
|
|
79
|
+
} catch (_) {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Minimal HTML-entity decode for & < > " ' ' &#nnn;
|
|
85
|
+
function decodeEntities(s) {
|
|
86
|
+
if (!s) return s;
|
|
87
|
+
return String(s)
|
|
88
|
+
.replace(/&/g, '&')
|
|
89
|
+
.replace(/</g, '<')
|
|
90
|
+
.replace(/>/g, '>')
|
|
91
|
+
.replace(/"/g, '"')
|
|
92
|
+
.replace(/'/g, "'")
|
|
93
|
+
.replace(/'/g, "'")
|
|
94
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) =>
|
|
95
|
+
String.fromCodePoint(parseInt(hex, 16))
|
|
96
|
+
)
|
|
97
|
+
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(parseInt(dec, 10)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} content - fetched page content
|
|
102
|
+
* @param {string|null} url - source URL (used for slug fallback)
|
|
103
|
+
* @returns {string} a non-empty trimmed title
|
|
104
|
+
*/
|
|
105
|
+
export function extractTitle(content, url) {
|
|
106
|
+
const safe = typeof content === 'string' ? content : '';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
fromMarkdownH1(safe) ||
|
|
110
|
+
fromHtmlTitle(safe) ||
|
|
111
|
+
fromHtmlH1(safe) ||
|
|
112
|
+
fromFirstLine(safe) ||
|
|
113
|
+
fromUrlSlug(url) ||
|
|
114
|
+
'Untitled document'
|
|
115
|
+
);
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memex-mvp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Local-first MCP server for cross-agent AI memory. One SQLite + FTS5 corpus across Claude Code, Cowork, Cursor, Continue, Zed, Obsidian, and Telegram — passively captured, verbatim, searchable from any MCP-compatible client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"sync": "node ingest.js",
|
|
27
27
|
"ingest": "node ingest.js",
|
|
28
28
|
"bot": "node bot/index.js",
|
|
29
|
-
"test": "node test/parser.test.js && node test/bot-inbox.test.js && node test/search-sort.test.js",
|
|
29
|
+
"test": "node test/parser.test.js && node test/bot-inbox.test.js && node test/search-sort.test.js && node test/store-document.test.js",
|
|
30
30
|
"prepublishOnly": "npm test"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
package/server.js
CHANGED
|
@@ -43,6 +43,13 @@ import {
|
|
|
43
43
|
KNOWN_SOURCES,
|
|
44
44
|
CONFIG_PATH,
|
|
45
45
|
} from './lib/config.js';
|
|
46
|
+
import {
|
|
47
|
+
canonicalize as canonicalizeUrl,
|
|
48
|
+
extractDomain,
|
|
49
|
+
} from './lib/store-doc/canonicalize.js';
|
|
50
|
+
import { detectIssues, isBlocked } from './lib/store-doc/detect.js';
|
|
51
|
+
import { extractTitle } from './lib/store-doc/extract-title.js';
|
|
52
|
+
import { createHash } from 'node:crypto';
|
|
46
53
|
|
|
47
54
|
// -------------------- Paths --------------------
|
|
48
55
|
const HOME = homedir();
|
|
@@ -952,6 +959,72 @@ Archived conversations are hidden from default list/search but stay
|
|
|
952
959
|
fully indexed. Pass include_archived: true on search/list to include
|
|
953
960
|
them. Visibility flag only — never deletes data.
|
|
954
961
|
|
|
962
|
+
══ DOCUMENT INGESTION (web pages, articles, AI chat shares) ══
|
|
963
|
+
|
|
964
|
+
memex_store_document accepts content YOU fetch and stores it verbatim.
|
|
965
|
+
Memex never fetches by itself — that's your job. Reasons:
|
|
966
|
+
• Memex stays 100% local (no outbound network egress)
|
|
967
|
+
• You have better tools (WebFetch, WebSearch, shell curl)
|
|
968
|
+
• You have context for error recovery (can ask user to paste)
|
|
969
|
+
|
|
970
|
+
THE JINA TRICK (full reference):
|
|
971
|
+
|
|
972
|
+
Modern web is mostly Cloudflare-protected. Server-side fetchers
|
|
973
|
+
(including most agent WebFetch implementations) hit a JS challenge and
|
|
974
|
+
return interstitial content instead of the page itself. The free
|
|
975
|
+
workaround is Jina AI Reader:
|
|
976
|
+
|
|
977
|
+
Original: https://example.com/whatever
|
|
978
|
+
Wrapped: https://r.jina.ai/https://example.com/whatever
|
|
979
|
+
|
|
980
|
+
This works for: Perplexity shared threads (must be Public!), npm.com
|
|
981
|
+
package pages, X/Twitter threads, Medium articles, Substack public
|
|
982
|
+
posts, most modern SaaS marketing pages.
|
|
983
|
+
|
|
984
|
+
This DOESN'T help for: login-walled content (paywall, private
|
|
985
|
+
accounts), SPA with no SSR (Jina gets empty initial HTML),
|
|
986
|
+
geo-restricted content.
|
|
987
|
+
|
|
988
|
+
DETECTION HEURISTIC — when to retry through Jina:
|
|
989
|
+
After your first WebFetch, retry through Jina if you see:
|
|
990
|
+
• HTTP 403 with header \`cf-mitigated: challenge\`
|
|
991
|
+
• Body contains "Just a moment..." / "Verifying you are human"
|
|
992
|
+
• Body contains \`cf-turnstile\` or \`cf_chl_opt\`
|
|
993
|
+
• Body < 500 bytes from a page that should be substantive
|
|
994
|
+
|
|
995
|
+
After Jina retry, if you get clean markdown — pass to memex_store_document.
|
|
996
|
+
If Jina ALSO returns "This thread is private" / "Sign in" / login form —
|
|
997
|
+
that's authentication, not Cloudflare. Tell the user what to do.
|
|
998
|
+
|
|
999
|
+
PERPLEXITY-SPECIFIC:
|
|
1000
|
+
Perplexity shared threads need to be marked "Public" by the owner.
|
|
1001
|
+
The URL in the user's address bar (perplexity.ai/search/<id>) is the
|
|
1002
|
+
OWNER'S private URL, not the shareable one. They must:
|
|
1003
|
+
1. Open the thread in Perplexity
|
|
1004
|
+
2. Click Share → toggle Public link
|
|
1005
|
+
3. Copy the new URL Perplexity shows
|
|
1006
|
+
4. Give you THAT URL
|
|
1007
|
+
|
|
1008
|
+
If memex_store_document returns warning type=perplexity-private,
|
|
1009
|
+
surface the message verbatim — it has the exact instructions.
|
|
1010
|
+
|
|
1011
|
+
PROACTIVE OFFER (TIMING):
|
|
1012
|
+
When user shares a URL they're DISCUSSING or RESEARCHING:
|
|
1013
|
+
"I can save this to your memex memory — you'll be able to search
|
|
1014
|
+
it from any AI chat later. Want me to?"
|
|
1015
|
+
|
|
1016
|
+
Don't offer for:
|
|
1017
|
+
• URLs you're already analyzing in the current turn
|
|
1018
|
+
• Same URL twice in one session
|
|
1019
|
+
• Casual passing mentions ("like example.com/foo")
|
|
1020
|
+
• URLs that are just navigation aids in conversation
|
|
1021
|
+
|
|
1022
|
+
Right moments:
|
|
1023
|
+
• User finished Perplexity research and is about to move on
|
|
1024
|
+
• User pasted a long article they want to discuss
|
|
1025
|
+
• User shared documentation they keep returning to
|
|
1026
|
+
• User shared an AI chat (ChatGPT/Claude.ai/Gemini share link)
|
|
1027
|
+
|
|
955
1028
|
══ ONBOARDING THE LAZY USER ══
|
|
956
1029
|
|
|
957
1030
|
The user probably hasn't read the README. Treat yourself as their
|
|
@@ -1436,6 +1509,61 @@ const TOOLS = [
|
|
|
1436
1509
|
},
|
|
1437
1510
|
},
|
|
1438
1511
|
},
|
|
1512
|
+
{
|
|
1513
|
+
name: 'memex_store_document',
|
|
1514
|
+
description:
|
|
1515
|
+
'Save an external document (web page, AI chat share, pasted text) to memex memory. Memex stores verbatim what you pass — it does NOT fetch URLs itself, that\'s your job (memex stays 100% local, no outbound network calls).\n\n' +
|
|
1516
|
+
'TYPICAL FLOW for a URL:\n' +
|
|
1517
|
+
' content = WebFetch("https://example.com/article")\n' +
|
|
1518
|
+
' memex_store_document(content, url="https://example.com/article", title="...")\n\n' +
|
|
1519
|
+
'⚠ THE JINA TRICK — memorize this:\n' +
|
|
1520
|
+
'Many modern pages (Perplexity, npm.com, X/Twitter, Medium, ChatGPT shares) sit behind Cloudflare bot-protection. Server-side fetches return "Just a moment..." / 403 / "cf-mitigated: challenge" instead of the real content.\n\n' +
|
|
1521
|
+
'Workaround: prepend `https://r.jina.ai/` to the URL.\n' +
|
|
1522
|
+
' Before: https://www.perplexity.ai/search/abc123\n' +
|
|
1523
|
+
' After: https://r.jina.ai/https://www.perplexity.ai/search/abc123\n\n' +
|
|
1524
|
+
'Jina AI Reader bypasses Cloudflare (runs a real browser server-side) and returns clean markdown. Free, no API key needed for personal use. Both http:// and https:// after `r.jina.ai/` work.\n\n' +
|
|
1525
|
+
'Heuristic — when to retry through Jina:\n' +
|
|
1526
|
+
' • HTTP 403 with cf-mitigated header\n' +
|
|
1527
|
+
' • Body contains "Just a moment..." / "Verifying you are human" / cf-turnstile\n' +
|
|
1528
|
+
' • Body < 500 bytes from a substantive page\n\n' +
|
|
1529
|
+
'PERPLEXITY THREADS need to be made PUBLIC first (Share → Public link in Perplexity). Private threads return "This thread is private" even via Jina — memex will detect this on store and tell you what to say to the user.\n\n' +
|
|
1530
|
+
'PRIVATE / LOGIN-WALLED content (paywall, your private ChatGPT chats) can\'t be fetched server-side. Tell the user — don\'t try to scrape.\n\n' +
|
|
1531
|
+
'PROACTIVE OFFER: When the user shares a substantive URL they\'re DISCUSSING or RESEARCHING (not just casually mentioning), offer to save it. Especially for Perplexity threads — that research is ephemeral and worth preserving.\n\n' +
|
|
1532
|
+
'Returns: {conversation_id, title, length, stored, warnings[]}. If stored=false, the `warnings` array tells you exactly what went wrong and how to fix it — surface that message to the user.',
|
|
1533
|
+
inputSchema: {
|
|
1534
|
+
type: 'object',
|
|
1535
|
+
properties: {
|
|
1536
|
+
content: {
|
|
1537
|
+
type: 'string',
|
|
1538
|
+
description:
|
|
1539
|
+
'The fetched page content as text or markdown. YOU (the agent) fetch this via WebFetch / curl / Jina. Memex stores it verbatim — no LLM processing, no summarization.',
|
|
1540
|
+
},
|
|
1541
|
+
url: {
|
|
1542
|
+
type: 'string',
|
|
1543
|
+
description:
|
|
1544
|
+
'The original source URL. Used for conversation_id (sha256 of canonical form → free deduplication), domain metadata, and the slug-based title fallback. Omit for non-URL pastes — memex will assign a content-hash-based synthetic id.',
|
|
1545
|
+
},
|
|
1546
|
+
title: {
|
|
1547
|
+
type: 'string',
|
|
1548
|
+
description:
|
|
1549
|
+
'Page title or document name. If omitted, memex extracts from content (markdown H1 → HTML title → URL slug → "Untitled document").',
|
|
1550
|
+
},
|
|
1551
|
+
tags: {
|
|
1552
|
+
type: 'array',
|
|
1553
|
+
items: { type: 'string' },
|
|
1554
|
+
description:
|
|
1555
|
+
'Optional tags stored in metadata (e.g. ["research", "perplexity"]). For future tag-based filtering. Lowercased and deduped on store.',
|
|
1556
|
+
},
|
|
1557
|
+
refresh: {
|
|
1558
|
+
type: 'boolean',
|
|
1559
|
+
default: false,
|
|
1560
|
+
description:
|
|
1561
|
+
'If a document with the same canonical URL was already ingested, set true to refetch and replace the stored content (the new message overwrites the old). Default false = skip with a "already in memex" note + the existing conversation_id.',
|
|
1562
|
+
},
|
|
1563
|
+
},
|
|
1564
|
+
required: ['content'],
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1439
1567
|
];
|
|
1440
1568
|
|
|
1441
1569
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
@@ -2374,6 +2502,196 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2374
2502
|
return textResult(lines.join('\n'));
|
|
2375
2503
|
}
|
|
2376
2504
|
|
|
2505
|
+
if (name === 'memex_store_document') {
|
|
2506
|
+
const content = typeof args.content === 'string' ? args.content : '';
|
|
2507
|
+
const rawUrl = typeof args.url === 'string' ? args.url.trim() : '';
|
|
2508
|
+
const explicitTitle = typeof args.title === 'string' ? args.title.trim() : '';
|
|
2509
|
+
const refresh = args.refresh === true;
|
|
2510
|
+
const tags = Array.isArray(args.tags)
|
|
2511
|
+
? Array.from(
|
|
2512
|
+
new Set(
|
|
2513
|
+
args.tags
|
|
2514
|
+
.filter((t) => typeof t === 'string')
|
|
2515
|
+
.map((t) => t.trim().toLowerCase())
|
|
2516
|
+
.filter(Boolean)
|
|
2517
|
+
)
|
|
2518
|
+
)
|
|
2519
|
+
: [];
|
|
2520
|
+
|
|
2521
|
+
if (!content.trim()) {
|
|
2522
|
+
return jsonResult({
|
|
2523
|
+
stored: false,
|
|
2524
|
+
conversation_id: null,
|
|
2525
|
+
title: null,
|
|
2526
|
+
length: 0,
|
|
2527
|
+
source: 'web',
|
|
2528
|
+
warnings: [
|
|
2529
|
+
{
|
|
2530
|
+
type: 'empty-content',
|
|
2531
|
+
blocking: true,
|
|
2532
|
+
message:
|
|
2533
|
+
'Content is empty. Pass the actual page text (you fetch it; memex stores it). ' +
|
|
2534
|
+
'For URLs you can\'t fetch (Cloudflare-blocked), retry through https://r.jina.ai/<original-url>.',
|
|
2535
|
+
},
|
|
2536
|
+
],
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
// Sniff for known failure patterns BEFORE storing
|
|
2541
|
+
const warnings = detectIssues(content, rawUrl);
|
|
2542
|
+
|
|
2543
|
+
if (isBlocked(warnings)) {
|
|
2544
|
+
return jsonResult({
|
|
2545
|
+
stored: false,
|
|
2546
|
+
conversation_id: null,
|
|
2547
|
+
title: null,
|
|
2548
|
+
length: content.length,
|
|
2549
|
+
source: 'web',
|
|
2550
|
+
url: rawUrl || null,
|
|
2551
|
+
warnings,
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// Build conversation_id: stable hash of canonical URL, or content hash for pastes
|
|
2556
|
+
let canonical = '';
|
|
2557
|
+
let convId;
|
|
2558
|
+
let captured_via;
|
|
2559
|
+
if (rawUrl) {
|
|
2560
|
+
canonical = canonicalizeUrl(rawUrl);
|
|
2561
|
+
const hash = createHash('sha256')
|
|
2562
|
+
.update(canonical)
|
|
2563
|
+
.digest('hex')
|
|
2564
|
+
.slice(0, 12);
|
|
2565
|
+
convId = `web-${hash}`;
|
|
2566
|
+
captured_via = 'mcp-tool';
|
|
2567
|
+
} else {
|
|
2568
|
+
const hash = createHash('sha256')
|
|
2569
|
+
.update(content)
|
|
2570
|
+
.digest('hex')
|
|
2571
|
+
.slice(0, 12);
|
|
2572
|
+
convId = `web-paste-${hash}`;
|
|
2573
|
+
captured_via = 'user-paste';
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// Check if already ingested
|
|
2577
|
+
const existing = db
|
|
2578
|
+
.prepare(
|
|
2579
|
+
`SELECT conversation_id, title, message_count FROM conversations WHERE conversation_id = ?`
|
|
2580
|
+
)
|
|
2581
|
+
.get(convId);
|
|
2582
|
+
|
|
2583
|
+
if (existing && !refresh) {
|
|
2584
|
+
return jsonResult({
|
|
2585
|
+
stored: false,
|
|
2586
|
+
already_ingested: true,
|
|
2587
|
+
conversation_id: existing.conversation_id,
|
|
2588
|
+
title: existing.title,
|
|
2589
|
+
length: content.length,
|
|
2590
|
+
source: 'web',
|
|
2591
|
+
url: rawUrl || null,
|
|
2592
|
+
warnings: [
|
|
2593
|
+
...warnings,
|
|
2594
|
+
{
|
|
2595
|
+
type: 'already-ingested',
|
|
2596
|
+
blocking: false,
|
|
2597
|
+
message:
|
|
2598
|
+
`This document is already in memex (conversation_id: ${existing.conversation_id}, title: "${existing.title}"). ` +
|
|
2599
|
+
'Call again with refresh=true to overwrite with the new content. ' +
|
|
2600
|
+
'Existing content can be retrieved via memex_get_conversation.',
|
|
2601
|
+
},
|
|
2602
|
+
],
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// Determine title (caller override → content extraction)
|
|
2607
|
+
const title = explicitTitle || extractTitle(content, rawUrl);
|
|
2608
|
+
const domain = rawUrl ? extractDomain(rawUrl) : null;
|
|
2609
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2610
|
+
|
|
2611
|
+
// msg_id is the ingest ts as string — unique per refetch, so refresh
|
|
2612
|
+
// doesn't collide with the previous version's UNIQUE constraint.
|
|
2613
|
+
const msgId = String(now);
|
|
2614
|
+
|
|
2615
|
+
const metadata = {
|
|
2616
|
+
url: rawUrl || null,
|
|
2617
|
+
canonical_url: canonical || null,
|
|
2618
|
+
title,
|
|
2619
|
+
fetched_via: 'agent',
|
|
2620
|
+
captured_via,
|
|
2621
|
+
domain: domain || null,
|
|
2622
|
+
fetched_at: now,
|
|
2623
|
+
tags,
|
|
2624
|
+
content_length: content.length,
|
|
2625
|
+
warnings_at_store: warnings.map((w) => w.type),
|
|
2626
|
+
};
|
|
2627
|
+
|
|
2628
|
+
try {
|
|
2629
|
+
// If refresh and a row already exists, drop the old message first so we
|
|
2630
|
+
// don't carry stale content. (UNIQUE is (source, conversation_id, msg_id);
|
|
2631
|
+
// a new msg_id wouldn't collide, but we want one message per URL by
|
|
2632
|
+
// convention.)
|
|
2633
|
+
if (existing && refresh) {
|
|
2634
|
+
db.prepare(
|
|
2635
|
+
`DELETE FROM messages WHERE source = 'web' AND conversation_id = ?`
|
|
2636
|
+
).run(convId);
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
insertMessage.run(
|
|
2640
|
+
'web',
|
|
2641
|
+
convId,
|
|
2642
|
+
msgId,
|
|
2643
|
+
'document',
|
|
2644
|
+
domain || 'web',
|
|
2645
|
+
content,
|
|
2646
|
+
now,
|
|
2647
|
+
JSON.stringify(metadata),
|
|
2648
|
+
now, // edited_at = ts for refresh ordering
|
|
2649
|
+
null // uuid — web docs don't have source uuids
|
|
2650
|
+
);
|
|
2651
|
+
|
|
2652
|
+
upsertConversation.run(
|
|
2653
|
+
convId,
|
|
2654
|
+
'web',
|
|
2655
|
+
title,
|
|
2656
|
+
now,
|
|
2657
|
+
now,
|
|
2658
|
+
1,
|
|
2659
|
+
null, // parent_conversation_id
|
|
2660
|
+
null // project_path
|
|
2661
|
+
);
|
|
2662
|
+
} catch (err) {
|
|
2663
|
+
log('store-document error:', err.message);
|
|
2664
|
+
return jsonResult({
|
|
2665
|
+
stored: false,
|
|
2666
|
+
conversation_id: null,
|
|
2667
|
+
title: null,
|
|
2668
|
+
length: content.length,
|
|
2669
|
+
source: 'web',
|
|
2670
|
+
url: rawUrl || null,
|
|
2671
|
+
warnings: [
|
|
2672
|
+
...warnings,
|
|
2673
|
+
{
|
|
2674
|
+
type: 'storage-error',
|
|
2675
|
+
blocking: true,
|
|
2676
|
+
message: `Couldn't write to memex DB: ${err.message}`,
|
|
2677
|
+
},
|
|
2678
|
+
],
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
return jsonResult({
|
|
2683
|
+
stored: true,
|
|
2684
|
+
conversation_id: convId,
|
|
2685
|
+
title,
|
|
2686
|
+
length: content.length,
|
|
2687
|
+
source: 'web',
|
|
2688
|
+
url: rawUrl || null,
|
|
2689
|
+
domain,
|
|
2690
|
+
refreshed: !!(existing && refresh),
|
|
2691
|
+
warnings,
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2377
2695
|
return textResult(`Unknown tool: ${name}`);
|
|
2378
2696
|
} catch (err) {
|
|
2379
2697
|
log('tool error:', name, err.message);
|
|
@@ -1,58 +1,106 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 📚 Install memex — cross-AI memory in 2 minutes
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
walks Claude Code (or any MCP-compatible agent that supports the Skills
|
|
5
|
-
spec) through installing **memex** on the user's machine — npm install,
|
|
6
|
-
MCP-config wiring, auto-capture daemon, and end-to-end verification.
|
|
3
|
+
> One prompt sets up local-first AI memory across **Claude Code, Cowork, Cursor, Cline, Continue, and Zed** — plus Obsidian notes and Telegram chats. No cloud. No account. No data leaves your machine.
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
## What this skill does
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
After you drop the skill into your agent (`~/.claude/skills/` for Claude Code, or your client's equivalent), saying **"install memex"** triggers a guided installation:
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
1. **Discovery** — read-only checks for which MCP client you're using and what AI data already exists on your machine
|
|
10
|
+
2. **`npm install -g memex-mvp`** — with EACCES fallbacks (one-shot sudo OR permanent prefix-fix; the agent asks you which)
|
|
11
|
+
3. **MCP config merge** — adds a single absolute-path `command` entry into your client's `mcpServers` config. Never overwrites your other servers
|
|
12
|
+
4. **`memex-sync install`** — registers the macOS LaunchAgent for live auto-capture
|
|
13
|
+
5. **`memex-sync scan`** — one-time backfill of every session that already exists on disk
|
|
14
|
+
6. **Restart hint + verification commands**
|
|
15
|
+
|
|
16
|
+
End-to-end: **~2 minutes**, fully observable (agent shows each command before running).
|
|
17
|
+
|
|
18
|
+
## What is memex?
|
|
19
|
+
|
|
20
|
+
Memex is a **local-first MCP server** that captures every conversation you have with an AI — across **Claude Code, Cowork (including subagent transcripts), Cursor, Cline, Continue, Zed**, plus **Obsidian notes**, **Telegram chats**, and **web pages / AI chat shares** (v0.6+ via `memex_store_document` — agent fetches, memex stores verbatim) — into one searchable SQLite + FTS5 corpus.
|
|
21
|
+
|
|
22
|
+
Any MCP-compatible agent can then query that corpus through 12 standard tools (`memex_search`, `memex_recent`, `memex_overview`, `memex_store_document`, …).
|
|
23
|
+
|
|
24
|
+
| Pain | Memex |
|
|
25
|
+
|-------------------------------------------------|--------------------------------------|
|
|
26
|
+
| Claude forgets every session | One unified history across all sessions, all clients |
|
|
27
|
+
| Switching from Claude Code to Cursor = lost context | Cursor can read your Claude history (same SQLite DB) |
|
|
28
|
+
| Cloud memory services hold your data hostage | One `~/.memex/data/memex.db` file on your disk |
|
|
29
|
+
| Mem0 / Supermemory pricing per token | 0 LLM calls on write — free at any scale |
|
|
30
|
+
| Vendor ban = lose everything | Your DB stays even if Claude blocks your account |
|
|
31
|
+
|
|
32
|
+
📦 npm: [memex-mvp](https://www.npmjs.com/package/memex-mvp) · 🏠 [memex.parallelclaw.ai](https://memex.parallelclaw.ai) · 🐛 [GitHub](https://github.com/parallelclaw/memex-mvp)
|
|
33
|
+
|
|
34
|
+
## Why a skill (vs. just copy-pasting the install prompt)
|
|
35
|
+
|
|
36
|
+
The install playbook also exists as a copy-paste prompt on the landing page. But once you have the skill installed, you skip a friction layer:
|
|
37
|
+
|
|
38
|
+
| Without skill | With skill |
|
|
39
|
+
|---|---|
|
|
40
|
+
| Open landing page in browser | (already in your agent) |
|
|
41
|
+
| Click "Copy install prompt" | — |
|
|
42
|
+
| Paste a wall of text into Claude/Cursor | Just say "install memex" |
|
|
43
|
+
| Hope the prompt is up-to-date | Skill version updates auto-fetched |
|
|
44
|
+
|
|
45
|
+
The skill description is written so Claude / OpenClaw auto-invoke it from any phrasing of "install memex", "set up memex", "add memory to my AI", etc. — no need to remember `/install-memex` exactly.
|
|
46
|
+
|
|
47
|
+
## Example dialogue
|
|
48
|
+
|
|
49
|
+
> **User:** install memex
|
|
50
|
+
>
|
|
51
|
+
> **Agent:** 👋 You're running inside Cursor. Let me run a quick discovery before any install actions...
|
|
52
|
+
>
|
|
53
|
+
> ✓ Found Claude Code data at `~/.claude/projects/` (8 sessions)
|
|
54
|
+
> ✓ Found Cowork sessions (3 subagent transcripts in your projects)
|
|
55
|
+
> ✗ Continue not installed
|
|
56
|
+
> ✗ Zed not installed
|
|
57
|
+
> ✓ Cursor data found (this is where I'll write the MCP config)
|
|
58
|
+
>
|
|
59
|
+
> After install, memex will auto-index conversations from: Claude Code, Cowork, Cursor. Drop Telegram exports into `~/.memex/inbox/` manually if you want those too.
|
|
60
|
+
>
|
|
61
|
+
> Ready to proceed with Step 1 (`npm install -g memex-mvp`)?
|
|
62
|
+
>
|
|
63
|
+
> **User:** yes
|
|
64
|
+
>
|
|
65
|
+
> **Agent:** [shows each command, runs npm install, gets `which memex` path, merges into `~/.cursor/mcp.json`, runs `memex-sync install`, runs `memex-sync scan`, prints restart hint]
|
|
66
|
+
|
|
67
|
+
See [examples.md](examples.md) for more sample dialogues, including EACCES recovery and partial-install scenarios.
|
|
68
|
+
|
|
69
|
+
## Requirements
|
|
70
|
+
|
|
71
|
+
- **Node.js 20–24** (LTS recommended). Install via [nodejs.org](https://nodejs.org), `brew install node`, or [nvm](https://github.com/nvm-sh/nvm)
|
|
72
|
+
- **macOS or Linux** (Windows untested for memex itself; skill works anywhere your agent has shell access)
|
|
73
|
+
- **An MCP-compatible CLI agent**: Claude Code, OpenClaw, Cursor (with MCP), Cline, Continue, or Zed. Web-only agents (ChatGPT, Claude.ai) can't execute install steps
|
|
74
|
+
|
|
75
|
+
## Install the skill
|
|
16
76
|
|
|
17
|
-
Or via curl from this repo:
|
|
18
77
|
```sh
|
|
19
78
|
mkdir -p ~/.claude/skills/install-memex
|
|
20
79
|
curl -fsSL https://raw.githubusercontent.com/parallelclaw/memex-mvp/main/skills/install-memex/SKILL.md \
|
|
21
80
|
-o ~/.claude/skills/install-memex/SKILL.md
|
|
22
81
|
```
|
|
23
82
|
|
|
24
|
-
|
|
83
|
+
Or, if you've already done `npm install -g memex-mvp`, the skill ships inside the package:
|
|
25
84
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
/install-memex
|
|
85
|
+
```sh
|
|
86
|
+
cp -r "$(npm root -g)/memex-mvp/skills/install-memex" ~/.claude/skills/
|
|
30
87
|
```
|
|
31
88
|
|
|
32
|
-
|
|
33
|
-
Claude picks it up automatically from natural-language requests.
|
|
89
|
+
Then in your agent: `install memex` (or `/install-memex` if your client supports slash-commands).
|
|
34
90
|
|
|
35
|
-
## What
|
|
91
|
+
## What the skill won't do
|
|
36
92
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
6. **Verification + restart hint.**
|
|
93
|
+
- ❌ Install Node.js for you — it stops and asks you to install it
|
|
94
|
+
- ❌ Run `sudo` without asking — EACCES fallback always confirms with you first
|
|
95
|
+
- ❌ Overwrite your existing `mcpServers` config — always merges
|
|
96
|
+
- ❌ Redirect any host application's data location — memex reads from each tool's native path
|
|
97
|
+
- ❌ Send anything over the network (besides `npm install` itself)
|
|
43
98
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
- **`/install-memex`** is shorter than "open the landing, click 'Copy install prompt', paste here"
|
|
47
|
-
- Skills are auto-discovered by name — `description` field lets Claude invoke this from any phrasing of "install memex"
|
|
48
|
-
- Future updates to the install flow auto-propagate if you re-fetch the skill, instead of relying on the user re-copying
|
|
49
|
-
|
|
50
|
-
## Related
|
|
99
|
+
## License
|
|
51
100
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- 📖 [Main memex repo](https://github.com/parallelclaw/memex-mvp)
|
|
101
|
+
[MIT](https://github.com/parallelclaw/memex-mvp/blob/main/LICENSE) for memex itself.
|
|
102
|
+
This skill bundle is published on ClawHub under [MIT-0](https://opensource.org/license/mit-0) (effectively public domain — no attribution required) per ClawHub's publishing terms.
|
|
55
103
|
|
|
56
|
-
|
|
104
|
+
---
|
|
57
105
|
|
|
58
|
-
|
|
106
|
+
📚 Built by [@parallelclaw](https://github.com/parallelclaw) · Star the repo if memex helps you: [github.com/parallelclaw/memex-mvp](https://github.com/parallelclaw/memex-mvp)
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: install-memex
|
|
3
|
-
description:
|
|
3
|
+
description: Make Claude, Cursor, Cline, Continue, and Zed remember every AI conversation forever — one local SQLite corpus shared across all of them. Installs memex (local-first MCP server) in ~2 minutes — npm install, MCP config wiring, auto-capture daemon, history backfill. No cloud, no account, verbatim storage. Also indexes Obsidian notes, Telegram chats, and any URL the user wants to save (web pages, Perplexity threads, AI chat shares — memex_store_document tool, v0.6+). Use when the user says "install memex", "set up memex", "add memory to my AI", "make my agent remember across sessions", or similar.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
metadata:
|
|
6
|
+
openclaw:
|
|
7
|
+
emoji: "📚"
|
|
8
|
+
homepage: https://memex.parallelclaw.ai
|
|
9
|
+
requires:
|
|
10
|
+
bins:
|
|
11
|
+
- node
|
|
12
|
+
- npm
|
|
4
13
|
---
|
|
5
14
|
|
|
6
15
|
# Install memex
|
|
@@ -151,6 +160,7 @@ After restart, suggest they try any of:
|
|
|
151
160
|
- "show me what memex has in memory" → triggers `memex_overview`
|
|
152
161
|
- "what projects has memex captured" → triggers `memex_list_projects`
|
|
153
162
|
- "search memex for [recent topic]" → triggers `memex_search`
|
|
163
|
+
- "save https://en.wikipedia.org/wiki/As_We_May_Think to memex" → triggers `memex_store_document` and teaches the user that URL-saving exists (v0.6+)
|
|
154
164
|
|
|
155
165
|
These confirm everything works end-to-end.
|
|
156
166
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Example dialogues
|
|
2
|
+
|
|
3
|
+
Sample interactions when the user invokes this skill. These show typical happy paths and how the skill handles common edge cases.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Example 1 · Happy path on a fresh Cursor install
|
|
8
|
+
|
|
9
|
+
> **User:** install memex
|
|
10
|
+
>
|
|
11
|
+
> **Agent:** 👋 You're running inside Cursor. Let me run a quick discovery before any install actions...
|
|
12
|
+
>
|
|
13
|
+
> ```
|
|
14
|
+
> ✓ Found Claude Code data at ~/.claude/projects/ (12 sessions)
|
|
15
|
+
> ✓ Found Cowork sessions (4 subagent transcripts)
|
|
16
|
+
> ✓ Cursor data found — I'll write MCP config to ~/.cursor/mcp.json
|
|
17
|
+
> ✗ Continue config not found
|
|
18
|
+
> ✗ Zed config not found
|
|
19
|
+
> ```
|
|
20
|
+
>
|
|
21
|
+
> After install, memex will auto-index conversations from: **Claude Code, Cowork, Cursor**. If you also use Obsidian or want Telegram chats indexed, we can add those after the main install.
|
|
22
|
+
>
|
|
23
|
+
> Ready to proceed with Step 1?
|
|
24
|
+
>
|
|
25
|
+
> **User:** yes
|
|
26
|
+
>
|
|
27
|
+
> **Agent:** Running `npm install -g memex-mvp`...
|
|
28
|
+
>
|
|
29
|
+
> ```
|
|
30
|
+
> + memex-mvp@0.5.4
|
|
31
|
+
> ```
|
|
32
|
+
>
|
|
33
|
+
> ✓ Memex installed. Verifying: `which memex` → `/usr/local/bin/memex`. Good.
|
|
34
|
+
>
|
|
35
|
+
> Step 2: I'm about to merge a new entry into your `~/.cursor/mcp.json`. Here's the diff:
|
|
36
|
+
>
|
|
37
|
+
> ```diff
|
|
38
|
+
> {
|
|
39
|
+
> "mcpServers": {
|
|
40
|
+
> "github": { ... },
|
|
41
|
+
> + "memex": {
|
|
42
|
+
> + "command": "/usr/local/bin/memex"
|
|
43
|
+
> + }
|
|
44
|
+
> }
|
|
45
|
+
> }
|
|
46
|
+
> ```
|
|
47
|
+
>
|
|
48
|
+
> Looks correct? (Y/n)
|
|
49
|
+
>
|
|
50
|
+
> **User:** yes
|
|
51
|
+
>
|
|
52
|
+
> **Agent:** ✓ Config saved.
|
|
53
|
+
>
|
|
54
|
+
> Step 3: `memex-sync install` — sets up the macOS LaunchAgent that auto-captures new Claude Code / Cowork / Cursor sessions...
|
|
55
|
+
>
|
|
56
|
+
> ```
|
|
57
|
+
> ✓ memex-sync installed
|
|
58
|
+
> ✓ daemon running (PID 47291)
|
|
59
|
+
> ✓ watching 12 sessions across 3 source roots
|
|
60
|
+
> ```
|
|
61
|
+
>
|
|
62
|
+
> Step 4: `memex-sync scan` to backfill existing history. This indexes everything already on your disk — might take 30-90 seconds...
|
|
63
|
+
>
|
|
64
|
+
> ```
|
|
65
|
+
> ingested 847 messages from 12 conversations
|
|
66
|
+
> ```
|
|
67
|
+
>
|
|
68
|
+
> ✓ All done. **Restart Cursor (Cmd+Q + reopen)** so it picks up the new memex tools. After restart, try:
|
|
69
|
+
>
|
|
70
|
+
> - "what does memex have in memory?"
|
|
71
|
+
> - "search memex for [a recent topic you worked on]"
|
|
72
|
+
> - "list my memex projects"
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Example 2 · EACCES recovery (macOS system Node)
|
|
77
|
+
|
|
78
|
+
> **User:** install memex
|
|
79
|
+
>
|
|
80
|
+
> **Agent:** [runs discovery, gets user OK] Step 1: `npm install -g memex-mvp`...
|
|
81
|
+
>
|
|
82
|
+
> ```
|
|
83
|
+
> npm error code EACCES
|
|
84
|
+
> npm error path: '/usr/local/lib/node_modules/memex-mvp'
|
|
85
|
+
> ```
|
|
86
|
+
>
|
|
87
|
+
> Hit a permissions error — this is common on macOS when Node is installed system-wide. Two ways to fix:
|
|
88
|
+
>
|
|
89
|
+
> **Option A** (quick, requires sudo password):
|
|
90
|
+
> ```sh
|
|
91
|
+
> sudo npm install -g memex-mvp
|
|
92
|
+
> ```
|
|
93
|
+
>
|
|
94
|
+
> **Option B** (one-time setup, then future `npm install -g` works without sudo):
|
|
95
|
+
> ```sh
|
|
96
|
+
> mkdir -p ~/.npm-global
|
|
97
|
+
> npm config set prefix ~/.npm-global
|
|
98
|
+
> echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc
|
|
99
|
+
> source ~/.zshrc
|
|
100
|
+
> npm install -g memex-mvp
|
|
101
|
+
> ```
|
|
102
|
+
>
|
|
103
|
+
> Option B is the standard npm recommendation — solves this for every future global install. Option A is faster if you don't want to touch your shell config. Which do you prefer?
|
|
104
|
+
>
|
|
105
|
+
> **User:** B
|
|
106
|
+
>
|
|
107
|
+
> **Agent:** [runs the 5 commands in sequence, shows output for each] ✓ Done. Verifying:
|
|
108
|
+
>
|
|
109
|
+
> ```
|
|
110
|
+
> which memex → /Users/macbook/.npm-global/bin/memex
|
|
111
|
+
> ```
|
|
112
|
+
>
|
|
113
|
+
> Now continuing to Step 2 with this path...
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Example 3 · Wants only Claude Code, not the daemon
|
|
118
|
+
|
|
119
|
+
> **User:** install memex but skip the daemon
|
|
120
|
+
>
|
|
121
|
+
> **Agent:** Sure — that means memex will be available as a tool, but new sessions won't be auto-captured. You'll need to run `memex-sync scan` manually whenever you want to index recent sessions. Got it?
|
|
122
|
+
>
|
|
123
|
+
> **User:** yes
|
|
124
|
+
>
|
|
125
|
+
> **Agent:** [runs Steps 1, 2, 4 — skipping `memex-sync install` in Step 3]
|
|
126
|
+
>
|
|
127
|
+
> ✓ Memex installed, MCP config wired, backfill complete (847 messages from existing sessions).
|
|
128
|
+
>
|
|
129
|
+
> Since you skipped the daemon, here's how to refresh memex going forward:
|
|
130
|
+
> ```sh
|
|
131
|
+
> memex-sync scan
|
|
132
|
+
> ```
|
|
133
|
+
>
|
|
134
|
+
> You can install the daemon any time later: `memex-sync install`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Example 4 · Existing memex install detected
|
|
139
|
+
|
|
140
|
+
> **User:** install memex
|
|
141
|
+
>
|
|
142
|
+
> **Agent:** Quick check: `which memex` → `/usr/local/bin/memex`. You already have memex installed.
|
|
143
|
+
>
|
|
144
|
+
> Running `memex-sync status`:
|
|
145
|
+
> ```
|
|
146
|
+
> daemon running (PID 47291), watching 12 sessions
|
|
147
|
+
> last capture: 4 minutes ago
|
|
148
|
+
> ```
|
|
149
|
+
>
|
|
150
|
+
> Memex is working. What would you like to do?
|
|
151
|
+
>
|
|
152
|
+
> - **Reinstall** (e.g. to update to the latest version): I can run `npm install -g memex-mvp@latest`
|
|
153
|
+
> - **Add a new source** (Obsidian vault, Telegram exports, etc.)
|
|
154
|
+
> - **Add memex to another MCP client** (you have it in Cursor; want it in Cline / Continue / Zed too?)
|
|
155
|
+
> - **Nothing** — already good
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Example 5 · Web-only agent (no shell access)
|
|
160
|
+
|
|
161
|
+
> **User:** install memex
|
|
162
|
+
>
|
|
163
|
+
> **Agent:** I see you're running me through claude.ai web — I don't have access to your machine's shell, so I can't run `npm install` or write files for you.
|
|
164
|
+
>
|
|
165
|
+
> You have three options to install memex:
|
|
166
|
+
>
|
|
167
|
+
> 1. **CLI agents** (best UX): Open Claude Code, Cursor, Cline, Continue, or Zed and ask me there. Those have shell access and will run this skill end-to-end.
|
|
168
|
+
>
|
|
169
|
+
> 2. **Manual install** (~5 minutes, you run the commands): Follow https://memex.parallelclaw.ai/#quickstart — 4 steps in your terminal.
|
|
170
|
+
>
|
|
171
|
+
> 3. **One-liner from terminal**:
|
|
172
|
+
> ```sh
|
|
173
|
+
> npm install -g memex-mvp
|
|
174
|
+
> memex-sync install
|
|
175
|
+
> ```
|
|
176
|
+
> Then add `{"mcpServers": {"memex": {"command": "$(which memex)"}}}` to your MCP client's config file. Details: https://memex.parallelclaw.ai
|
|
177
|
+
>
|
|
178
|
+
> Which path works for you?
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Example 6 · Right after install — testing URL save (v0.6+)
|
|
185
|
+
|
|
186
|
+
> **User:** save https://en.wikipedia.org/wiki/As_We_May_Think to memex
|
|
187
|
+
>
|
|
188
|
+
> **Agent:** Fetching the page with WebFetch...
|
|
189
|
+
>
|
|
190
|
+
> ```
|
|
191
|
+
> ✓ Got 12,847 chars from en.wikipedia.org
|
|
192
|
+
> ```
|
|
193
|
+
>
|
|
194
|
+
> Calling `memex_store_document(content, url=https://en.wikipedia.org/wiki/As_We_May_Think, title=auto)`...
|
|
195
|
+
>
|
|
196
|
+
> ```
|
|
197
|
+
> ✓ stored
|
|
198
|
+
> conversation_id: web-d3b07a384d1b
|
|
199
|
+
> title: "As We May Think"
|
|
200
|
+
> length: 12,847 chars
|
|
201
|
+
> warnings: []
|
|
202
|
+
> ```
|
|
203
|
+
>
|
|
204
|
+
> Saved. Try searching it now: *"search memex for memex Bush 1945"* — the article will show up alongside any AI chats where you discussed it.
|
|
205
|
+
>
|
|
206
|
+
> 💡 By the way, you can save any URL this way — Perplexity threads, blog posts, ChatGPT shares, Twitter threads, GitHub discussions. For Perplexity specifically, the thread must be made Public first (Share → Public link). For Cloudflare-protected sites, memex auto-falls back through `r.jina.ai` proxy.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## What these examples illustrate about the skill's behavior
|
|
211
|
+
|
|
212
|
+
- **Always discover first**, then ask for confirmation before any write action
|
|
213
|
+
- **Show every command** before running it; show diffs for config edits
|
|
214
|
+
- **Offer choices** at decision points (EACCES, daemon yes/no) rather than guessing
|
|
215
|
+
- **Detect and adapt** to existing installs, missing prerequisites, partial setups
|
|
216
|
+
- **Refuse gracefully** when running in a context where the install can't actually work (web-only agents)
|
|
217
|
+
|
|
218
|
+
If you see the skill *not* doing one of these things, that's a bug — report it at https://github.com/parallelclaw/memex-mvp/issues.
|