memex-mvp 0.7.0 → 0.8.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 CHANGED
@@ -301,6 +301,10 @@ Memex по дефолту сортирует по **релевантности**
301
301
  memex search "Postgres миграция" # FTS5 поиск
302
302
  memex search "Q2 deck" --chat "Memex Bot" # фильтр по title чата
303
303
  memex search "auth" --source claude-code --limit 5 --sort date_desc
304
+ memex search "JWT" --as-of 2026-05-01 # time-travel (v0.8.1+) — только до даты
305
+
306
+ memex when "Brian Chesky" # «когда мы об этом говорили»
307
+ memex when "JWT decision" --limit 5 # → chronological list of chats, без snippets
304
308
 
305
309
  memex recent --limit 5 # последние сообщения
306
310
  memex recent --source telegram
@@ -310,7 +314,7 @@ memex list --source web # только сохранё
310
314
 
311
315
  memex get web-1582ab51a7b7 # полный контент conversation
312
316
 
313
- memex overview # snapshot корпуса
317
+ memex overview # snapshot корпуса (+ capture streak v0.8.1+)
314
318
  memex projects # уникальные project_paths
315
319
  memex help # эта инструкция в терминале
316
320
  memex --help # справка по командам
@@ -338,6 +342,35 @@ memex get web-1582ab51a7b7 --json > backup.json
338
342
 
339
343
  ---
340
344
 
345
+ ## 🪄 Auto-context (v0.8+) — Brian Chesky moment
346
+
347
+ Magic-фича. Когда ты открываешь Claude Code в проекте, Claude **сам** инжектит 500-1500 токенов контекста про этот проект — что ты делал недавно, какие conversations касались темы. Ты ещё ничего не спросил, а AI **уже знает**.
348
+
349
+ **Технически:** SessionStart hook в `~/.claude/settings.json`. При старте каждой Claude Code сессии хук вызывает `memex context` → memex выдаёт markdown summary → Claude получает его как system message _до_ твоего первого вопроса.
350
+
351
+ **Установка:** во время `memex-sync install` будет промпт `[Y/n]` — соглашайся (Y по default'у). Или установи позже:
352
+
353
+ ```bash
354
+ memex hook install # добавить хук
355
+ memex hook uninstall # удалить (только memex-запись, другие хуки сохраняются)
356
+ memex hook status # узнать текущее состояние
357
+ ```
358
+
359
+ **Посмотреть что будет инжектиться** (dry-run в текущей директории):
360
+
361
+ ```bash
362
+ memex context # markdown как для хука
363
+ memex context --json # структурированный
364
+ memex context --no-source telegram # исключить telegram (privacy)
365
+ memex context --freshness-days 30 # только последние 30 дней
366
+ ```
367
+
368
+ **Privacy:** хук ничего не отправляет наружу — это локальная инъекция в локальную Claude-сессию. Но в context могут попасть фрагменты из любых indexed sources (включая Telegram). Чтобы исключить — добавь `--no-source telegram` в команду хука (правится в `~/.claude/settings.json`).
369
+
370
+ **Где это пока не работает:** Cursor, Cline, Continue, Zed — у них нет native SessionStart hook'а. Fallback через MCP-tool — в roadmap v0.9.0. Сейчас auto-context работает только в **Claude Code и OpenClaw**.
371
+
372
+ ---
373
+
341
374
  ## Если что-то не работает
342
375
 
343
376
  ### Поиск пустой
package/README.md CHANGED
@@ -107,10 +107,12 @@ The same `memex` binary that runs as an MCP server also has a terminal mode for
107
107
  ```sh
108
108
  memex search "Postgres migration" # full-text search
109
109
  memex search "Q2 deck" --chat "Memex Bot" # scope to one conversation by title
110
+ memex search "JWT" --as-of 2026-05-01 # v0.8.1: time-travel — only msgs before date
111
+ memex when "Brian Chesky" # v0.8.1: "when did we talk about X" — dates + chats
110
112
  memex recent --limit 5 # last 5 messages across all sources
111
113
  memex list --source web # all saved URLs
112
114
  memex get web-1582ab51a7b7 # full content of one conversation
113
- memex overview # snapshot of corpus
115
+ memex overview # snapshot of corpus + v0.8.1: capture streak
114
116
  memex projects # distinct project_paths captured
115
117
  memex help # full user guide (HELP.md)
116
118
  memex --help # command reference
@@ -122,6 +124,28 @@ When called **without arguments** (`memex`), the binary still runs as an MCP std
122
124
 
123
125
  ---
124
126
 
127
+ ## Auto-context (v0.8+) — Claude already knows what you were doing
128
+
129
+ After `memex-sync install`, you're prompted to enable **auto-context**. When yes, memex adds a SessionStart hook to `~/.claude/settings.json` so that **every time you open Claude Code in a project**, Claude gets injected with ~500-1500 tokens of relevant context — what you did recently in this project, which conversations touched it, which related topics came up. No prompts. No tool calls. Just memory.
130
+
131
+ ```sh
132
+ # Adding/removing the hook outside the install flow:
133
+ memex hook install # add SessionStart hook (idempotent)
134
+ memex hook uninstall # remove only the memex entry, preserves other hooks
135
+ memex hook status # show current state
136
+
137
+ # Inspecting what gets injected:
138
+ memex context # dry-run the hook output for the current dir
139
+ memex context --pwd /path # for a different project
140
+ memex context --no-source telegram # exclude a source
141
+ ```
142
+
143
+ The hook respects existing hooks (e.g. `gstack`, custom user hooks) — they're preserved untouched.
144
+
145
+ **Currently only Claude Code has native SessionStart hooks.** For Cursor / Cline / Continue / Zed, MCP-tool-based fallback is on the v0.9.0 roadmap.
146
+
147
+ ---
148
+
125
149
  ## Save URLs into memex (v0.6+)
126
150
 
127
151
  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, …:
package/README.ru.md CHANGED
@@ -143,10 +143,12 @@ curl -fsSL https://raw.githubusercontent.com/parallelclaw/memex-mvp/main/skills/
143
143
  ```bash
144
144
  memex search "Postgres миграция" # полнотекстовый поиск
145
145
  memex search "Q2 deck" --chat "Memex Bot" # сузить до конкретного чата по title
146
+ memex search "JWT" --as-of 2026-05-01 # v0.8.1: time-travel — только до даты
147
+ memex when "Brian Chesky" # v0.8.1: «когда мы это обсуждали» — даты + чаты
146
148
  memex recent --limit 5 # последние 5 сообщений из всех источников
147
149
  memex list --source web # все сохранённые URL'ы
148
150
  memex get web-1582ab51a7b7 # полный контент одной conversation
149
- memex overview # snapshot корпуса
151
+ memex overview # snapshot корпуса + v0.8.1: capture streak
150
152
  memex projects # уникальные project_paths
151
153
  memex help # полное руководство (HELP.md)
152
154
  memex --help # справка по командам
@@ -162,6 +164,26 @@ memex --help # справка по команд
162
164
  - Хочешь пайпить результаты: `memex search foo --json | jq ...`
163
165
  - Хочешь сдампить полный transcript в stdout для context'a
164
166
 
167
+ ### Auto-context (v0.8+) — Claude уже знает что ты делал
168
+
169
+ После `memex-sync install` появляется промпт про **auto-context**. Если согласишься — memex добавит SessionStart хук в `~/.claude/settings.json`. Когда ты потом открываешь Claude Code в каком-то проекте, Claude **сам подгружает 500-1500 токенов контекста** про этот проект — что ты делал недавно, какие conversations его касались, какие связанные темы всплывали. Никаких вопросов, никаких tool-call'ов, просто Claude **знает**.
170
+
171
+ ```bash
172
+ # Добавить/удалить хук вне install-flow:
173
+ memex hook install # добавить SessionStart хук (idempotent)
174
+ memex hook uninstall # удалить только memex-запись, остальные хуки не трогает
175
+ memex hook status # показать текущее состояние
176
+
177
+ # Посмотреть что будет инжектиться:
178
+ memex context # dry-run для текущей директории
179
+ memex context --pwd /path # для другого проекта
180
+ memex context --no-source telegram # исключить источник
181
+ ```
182
+
183
+ Хук **сохраняет существующие хуки** (gstack, твои кастомные) — добавляет только свою запись.
184
+
185
+ **Сейчас native SessionStart есть только в Claude Code.** Для Cursor / Cline / Continue / Zed fallback через MCP-tool — в roadmap v0.9.0.
186
+
165
187
  ### Подключение к Claude Code
166
188
 
167
189
  Сначала возьми **два абсолютных пути** в терминале:
package/ingest.js CHANGED
@@ -61,6 +61,8 @@ import {
61
61
  vaultSlug,
62
62
  shouldSkipPath,
63
63
  } from './lib/parse-obsidian.js';
64
+ import { installHook as installSessionStartHook } from './lib/hook/install.js';
65
+ import { createInterface } from 'node:readline';
64
66
  import {
65
67
  CONFIG_PATH,
66
68
  KNOWN_SOURCES,
@@ -137,7 +139,12 @@ if (subcommand && subcommand !== '--help' && subcommand.startsWith('-') === fals
137
139
  console.error(`usage: memex-sync [install|uninstall|status|logs|serve]`);
138
140
  process.exit(2);
139
141
  }
140
- handler();
142
+ // Handlers may be sync (most) or async (cmdInstall after v0.8 — needs readline
143
+ // for the auto-context prompt). Promise.resolve() normalises both.
144
+ Promise.resolve(handler()).catch((e) => {
145
+ console.error(`error in ${subcommand}: ${e.stack || e.message}`);
146
+ process.exit(1);
147
+ });
141
148
  // CLI handlers either exit themselves or fall through to daemon mode (cmdServe)
142
149
  } else if (subcommand === '--help' || subcommand === '-h') {
143
150
  console.log(`memex-sync — auto-capture daemon for memex memory
@@ -185,7 +192,7 @@ paths:
185
192
 
186
193
  // -------------------- CLI command handlers --------------------
187
194
 
188
- function cmdInstall() {
195
+ async function cmdInstall() {
189
196
  if (platform() !== 'darwin') {
190
197
  console.error('install: macOS-only for now (LaunchAgent). Linux systemd-user support pending.');
191
198
  console.error('on Linux you can run: nohup memex-sync &');
@@ -243,6 +250,18 @@ function cmdInstall() {
243
250
  console.log(` log: ${LOG_PATH}`);
244
251
  console.log('');
245
252
 
253
+ // ── Auto-context prompt (v0.8+) ─────────────────────────────────────
254
+ // Bundle Claude Code SessionStart hook install into the same flow
255
+ // the user is already running. Single [Y/n] beats a separate command
256
+ // they'd never remember.
257
+ //
258
+ // Honor non-interactive flags / env for CI:
259
+ // --auto-context yes explicit opt-in
260
+ // --auto-context no explicit opt-out
261
+ // --yes / -y accept all defaults (yes)
262
+ // $MEMEX_AUTO_CONTEXT=yes|no env override
263
+ await maybeInstallAutoContextHook();
264
+
246
265
  // Show what daemon will actually capture, based on current config.
247
266
  const cfg = loadConfig();
248
267
  console.log('memex-sync will capture from these sources:');
@@ -277,6 +296,97 @@ function cmdInstall() {
277
296
  process.exit(0);
278
297
  }
279
298
 
299
+ // ──────────────────────────────────────────────────────────────────
300
+ // Auto-context hook prompt — bundled into `memex-sync install` so the
301
+ // user doesn't need to remember an extra `memex hook install` step.
302
+ //
303
+ // Decision priority:
304
+ // 1. CLI flag --auto-context=yes|no (explicit)
305
+ // 2. CLI flag --yes / -y (accept all defaults: yes)
306
+ // 3. env MEMEX_AUTO_CONTEXT=yes|no (for CI / scripts)
307
+ // 4. Interactive [Y/n] prompt (TTY only)
308
+ // 5. Default: SKIP if no TTY (don't hang on stdin in non-TTY contexts)
309
+ async function maybeInstallAutoContextHook() {
310
+ const argv = process.argv.slice(3); // drop ["node", "ingest.js", "install"]
311
+
312
+ // Parse flags
313
+ let explicit = null; // 'yes' | 'no' | null
314
+ for (let i = 0; i < argv.length; i++) {
315
+ const a = argv[i];
316
+ if (a === '--auto-context') {
317
+ const v = (argv[++i] || '').toLowerCase();
318
+ if (v === 'yes' || v === 'y' || v === 'true') explicit = 'yes';
319
+ else if (v === 'no' || v === 'n' || v === 'false') explicit = 'no';
320
+ } else if (a === '--auto-context=yes') explicit = 'yes';
321
+ else if (a === '--auto-context=no') explicit = 'no';
322
+ else if (a === '--yes' || a === '-y') explicit = 'yes';
323
+ }
324
+
325
+ // Env fallback
326
+ if (explicit === null) {
327
+ const env = (process.env.MEMEX_AUTO_CONTEXT || '').toLowerCase();
328
+ if (env === 'yes' || env === 'y' || env === 'true' || env === '1') explicit = 'yes';
329
+ else if (env === 'no' || env === 'n' || env === 'false' || env === '0') explicit = 'no';
330
+ }
331
+
332
+ // Interactive prompt as last resort
333
+ if (explicit === null) {
334
+ if (!process.stdin.isTTY) {
335
+ // Non-interactive (CI, scripts, install-skill flows that don't pipe stdin):
336
+ // skip silently. User can run `memex hook install` later.
337
+ console.log('Auto-context hook: skipped (non-interactive). Enable with: memex hook install');
338
+ console.log('');
339
+ return;
340
+ }
341
+ explicit = await promptYesNo(
342
+ `Auto-context (Brian Chesky mode):\n` +
343
+ ` When you open Claude Code in a project, memex can inject 500-1500 tokens\n` +
344
+ ` of relevant context so Claude knows what you were doing — without you\n` +
345
+ ` having to ask. Adds a SessionStart hook to ~/.claude/settings.json.\n` +
346
+ ` Other hooks (e.g. gstack) are preserved.\n\n` +
347
+ ` Enable?`,
348
+ 'yes' // default Y
349
+ );
350
+ }
351
+
352
+ if (explicit !== 'yes') {
353
+ console.log('Auto-context hook: skipped. Enable later with: memex hook install');
354
+ console.log('');
355
+ return;
356
+ }
357
+
358
+ const r = installSessionStartHook();
359
+ if (r.error) {
360
+ console.log(`Auto-context hook: ✗ ${r.error}`);
361
+ console.log(' (memex-sync daemon still works — only the auto-context hook failed)');
362
+ console.log('');
363
+ return;
364
+ }
365
+ if (r.alreadyPresent) {
366
+ console.log('Auto-context hook: already installed (no-op).');
367
+ } else {
368
+ console.log('Auto-context hook: ✓ installed.');
369
+ console.log(` settings: ${r.settingsPath}`);
370
+ console.log(` command: ${r.command}`);
371
+ console.log(' Restart Claude Code (Cmd+Q + reopen) to activate.');
372
+ }
373
+ console.log('');
374
+ }
375
+
376
+ function promptYesNo(question, defaultAnswer) {
377
+ return new Promise((resolve) => {
378
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
379
+ const suffix = defaultAnswer === 'yes' ? ' [Y/n] ' : ' [y/N] ';
380
+ rl.question(question + suffix, (answer) => {
381
+ rl.close();
382
+ const v = (answer || '').trim().toLowerCase();
383
+ if (v === 'y' || v === 'yes') resolve('yes');
384
+ else if (v === 'n' || v === 'no') resolve('no');
385
+ else resolve(defaultAnswer); // empty enter → default
386
+ });
387
+ });
388
+ }
389
+
280
390
  function cmdUninstall() {
281
391
  if (platform() !== 'darwin') {
282
392
  console.error('uninstall: macOS-only for now.');
package/lib/cli/index.js CHANGED
@@ -24,15 +24,22 @@
24
24
  */
25
25
 
26
26
  import Database from 'better-sqlite3';
27
- import { join } from 'node:path';
27
+ import { join, basename } from 'node:path';
28
28
  import { homedir } from 'node:os';
29
29
  import { existsSync, readFileSync } from 'node:fs';
30
30
  import { fileURLToPath } from 'node:url';
31
+ import {
32
+ installHook,
33
+ uninstallHook,
34
+ getHookStatus,
35
+ resolveMemexBinPath,
36
+ } from '../hook/install.js';
31
37
 
32
38
  // ---------- Subcommand registry ----------
33
39
  export const CLI_SUBCOMMAND_NAMES = [
34
40
  'search', 'recent', 'list', 'get', 'overview',
35
- 'projects', 'help', '-h', '--help', '-v', '--version',
41
+ 'projects', 'context', 'hook', 'when',
42
+ 'help', '-h', '--help', '-v', '--version',
36
43
  ];
37
44
 
38
45
  // ---------- Path helpers ----------
@@ -71,6 +78,15 @@ function parseArgs(argv) {
71
78
  else if (a === '--project') opts.project = argv[++i];
72
79
  else if (a === '--sort') opts.sort = argv[++i];
73
80
  else if (a === '--include-archived') opts.includeArchived = true;
81
+ else if (a === '--pwd') opts.pwd = argv[++i];
82
+ else if (a === '--budget' || a === '--budget-tokens') opts.budget = parseInt(argv[++i], 10);
83
+ else if (a === '--freshness-days') opts.freshnessDays = parseInt(argv[++i], 10);
84
+ else if (a === '--no-source') {
85
+ // Allow repeated --no-source telegram --no-source obsidian
86
+ if (!Array.isArray(opts.noSource)) opts.noSource = [];
87
+ opts.noSource.push(argv[++i]);
88
+ }
89
+ else if (a === '--as-of') opts.asOf = argv[++i];
74
90
  else if (a === '--help' || a === '-h') opts.help = true;
75
91
  else if (a.startsWith('--')) { /* ignore unknown flag for forward-compat */ }
76
92
  else positionals.push(a);
@@ -98,6 +114,20 @@ function fmtDateTime(ts) {
98
114
  return new Date(ts * 1000).toISOString().slice(0, 16).replace('T', ' ');
99
115
  }
100
116
 
117
+ /**
118
+ * Parse YYYY-MM-DD into unix timestamp at start-of-day (00:00 UTC).
119
+ * Returns null on invalid input.
120
+ */
121
+ function parseAsOf(s) {
122
+ if (typeof s !== 'string') return null;
123
+ const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
124
+ if (!m) return null;
125
+ const [, y, mo, d] = m;
126
+ const date = new Date(`${y}-${mo}-${d}T00:00:00Z`);
127
+ if (isNaN(date.getTime())) return null;
128
+ return Math.floor(date.getTime() / 1000);
129
+ }
130
+
101
131
  // FTS5 expects sanitized tokens — strip what would be operators
102
132
  function sanitizeFtsQuery(q) {
103
133
  return String(q || '')
@@ -142,6 +172,18 @@ async function cmdSearch(args) {
142
172
  filters.push('LOWER(c.title) LIKE LOWER(?)');
143
173
  params.push(`%${opts.chat}%`);
144
174
  }
175
+ // Time-travel: --as-of YYYY-MM-DD returns only messages with ts strictly
176
+ // before that calendar date (start-of-day). Useful for retrospectives:
177
+ // "what did I know about X two weeks ago?"
178
+ if (opts.asOf) {
179
+ const cutoff = parseAsOf(opts.asOf);
180
+ if (cutoff === null) {
181
+ console.error(`Invalid --as-of date: "${opts.asOf}". Expected YYYY-MM-DD.`);
182
+ process.exit(2);
183
+ }
184
+ filters.push('m.ts > 0 AND m.ts < ?');
185
+ params.push(cutoff);
186
+ }
145
187
 
146
188
  let orderBy;
147
189
  if (opts.sort === 'date_asc') {
@@ -345,6 +387,10 @@ async function cmdOverview(args) {
345
387
  ORDER BY last_ts DESC
346
388
  LIMIT 10
347
389
  `).all();
390
+
391
+ // Streak + today's capture count (D6 — GitHub-style daily habit signal)
392
+ const streak = computeStreak(db);
393
+
348
394
  db.close();
349
395
 
350
396
  if (opts.json) {
@@ -353,11 +399,25 @@ async function cmdOverview(args) {
353
399
  total_conversations: totalConvs,
354
400
  sources,
355
401
  recent_conversations: recentConvs,
402
+ streak: streak,
356
403
  }, null, 2));
357
404
  return;
358
405
  }
359
406
  console.log(c.bold('memex corpus snapshot') + '\n');
360
407
  console.log(`Total: ${c.green(totalMsgs + ' messages')} in ${c.green(totalConvs + ' conversations')}\n`);
408
+
409
+ // Streak block — only show if there's at least one captured day
410
+ if (streak.streakDays > 0) {
411
+ const today = streak.todayMessages;
412
+ const todayLine = today > 0
413
+ ? `Today: ${c.green(today + ' messages')} across ${streak.todayConversations} conversation(s).`
414
+ : `${c.dim('No captures yet today.')}`;
415
+ const streakLine = streak.streakDays >= 2
416
+ ? `${c.green('✓ ' + streak.streakDays + '-day capture streak')} (since ${fmtDate(streak.streakStartTs)}). ${todayLine}`
417
+ : `${c.dim('Starting fresh — capture something today to begin a streak.')} ${todayLine}`;
418
+ console.log(streakLine + '\n');
419
+ }
420
+
361
421
  console.log(c.bold('By source:'));
362
422
  for (const s of sources) {
363
423
  console.log(` ${s.source.padEnd(18)} ${String(s.msgs).padStart(7)} msgs · ${String(s.chats).padStart(5)} chats · ${fmtDate(s.first_ts)} → ${fmtDate(s.last_ts)}`);
@@ -369,6 +429,66 @@ async function cmdOverview(args) {
369
429
  }
370
430
  }
371
431
 
432
+ /**
433
+ * Compute current capture streak: consecutive days (working backward from
434
+ * today) with at least one message captured.
435
+ *
436
+ * Returns:
437
+ * {
438
+ * streakDays: number, // 0 if today has 0 captures
439
+ * streakStartTs: number, // ts of the earliest day in the streak
440
+ * todayMessages: number, // count of messages captured today
441
+ * todayConversations: number,
442
+ * }
443
+ *
444
+ * "Day" boundaries are UTC (matches how we store ts). A more user-friendly
445
+ * version would use local-day, but UTC is consistent and predictable —
446
+ * good enough for v0.8.1.
447
+ */
448
+ function computeStreak(db) {
449
+ const now = Math.floor(Date.now() / 1000);
450
+ const todayStart = Math.floor(now / 86400) * 86400; // UTC midnight today
451
+
452
+ // Distinct days with captures, sorted desc — pull up to ~365 days to bound work
453
+ const days = db.prepare(`
454
+ SELECT DISTINCT (ts / 86400) AS day
455
+ FROM messages
456
+ WHERE ts >= ?
457
+ ORDER BY day DESC
458
+ `).all(todayStart - 365 * 86400);
459
+
460
+ let streakDays = 0;
461
+ let streakStartTs = 0;
462
+ if (days.length > 0) {
463
+ const todayDay = Math.floor(todayStart / 86400);
464
+ let cursor = todayDay;
465
+ for (const row of days) {
466
+ if (row.day === cursor) {
467
+ streakDays += 1;
468
+ streakStartTs = row.day * 86400;
469
+ cursor -= 1;
470
+ } else if (row.day < cursor) {
471
+ // Streak broken — stop
472
+ break;
473
+ }
474
+ // row.day > cursor shouldn't happen with DESC order; if it does, skip
475
+ }
476
+ }
477
+
478
+ const todayRow = db.prepare(`
479
+ SELECT COUNT(*) AS msgs, COUNT(DISTINCT conversation_id) AS convs
480
+ FROM messages
481
+ WHERE ts >= ?
482
+ `).get(todayStart);
483
+
484
+ return {
485
+ streakDays,
486
+ streakStartTs,
487
+ todayMessages: todayRow.msgs,
488
+ todayConversations: todayRow.convs,
489
+ };
490
+ }
491
+
372
492
  // =============================================================
373
493
  // PROJECTS
374
494
  // =============================================================
@@ -400,6 +520,348 @@ async function cmdProjects(args) {
400
520
  }
401
521
  }
402
522
 
523
+ // =============================================================
524
+ // WHEN — chronological "when did we talk about X" CLI shortcut
525
+ // =============================================================
526
+ //
527
+ // `memex when "JWT decision"` answers the single most common memex query
528
+ // in 1 second: dates, sources, conversation titles. No snippets, no
529
+ // re-ranking — just chronological recall.
530
+ //
531
+ // Returns one row per matching conversation, sorted by latest message in
532
+ // that conversation (date_desc). Useful when the user remembers a topic
533
+ // but can't recall WHICH session it came from or WHEN.
534
+ async function cmdWhen(args) {
535
+ const { opts, positionals } = parseArgs(args);
536
+ const query = positionals.join(' ').trim();
537
+
538
+ if (!query || opts.help) {
539
+ console.error('Usage: memex when "<query>" [--source X] [--limit N] [--json]');
540
+ console.error('');
541
+ console.error('Returns a chronological "when did we talk about X" list — date + source +');
542
+ console.error('conversation title, no snippets. Sorted newest first.');
543
+ process.exit(query ? 0 : 2);
544
+ }
545
+
546
+ const limit = Math.min(50, Math.max(1, opts.limit || 15));
547
+ const sanitized = sanitizeFtsQuery(query);
548
+ if (!sanitized) {
549
+ console.error('Query became empty after sanitization — try simpler keywords.');
550
+ process.exit(2);
551
+ }
552
+
553
+ const filters = ['messages_fts MATCH ?'];
554
+ const params = [sanitized];
555
+ if (opts.source) {
556
+ filters.push('m.source = ?');
557
+ params.push(opts.source);
558
+ }
559
+ if (!opts.includeArchived) {
560
+ filters.push('(c.archived_at IS NULL OR c.archived_at = 0)');
561
+ }
562
+
563
+ // Aggregate by conversation: one row per chat, latest hit's date, match count
564
+ const sql = `
565
+ SELECT m.conversation_id,
566
+ m.source,
567
+ MAX(m.ts) AS latest_ts,
568
+ MIN(m.ts) AS earliest_ts,
569
+ COUNT(*) AS match_count,
570
+ c.title AS conversation_title
571
+ FROM messages_fts
572
+ JOIN messages m ON m.id = messages_fts.rowid
573
+ LEFT JOIN conversations c ON c.conversation_id = m.conversation_id
574
+ WHERE ${filters.join(' AND ')}
575
+ GROUP BY m.conversation_id
576
+ ORDER BY latest_ts DESC
577
+ LIMIT ?
578
+ `;
579
+ const db = openDb();
580
+ const rows = db.prepare(sql).all(...params, limit);
581
+ db.close();
582
+
583
+ if (opts.json) {
584
+ console.log(JSON.stringify({ query, count: rows.length, results: rows }, null, 2));
585
+ return;
586
+ }
587
+ if (rows.length === 0) {
588
+ console.log(`No mentions of ${c.bold('"' + query + '"')} found.`);
589
+ return;
590
+ }
591
+ console.log(`${c.bold('"' + query + '"')} mentioned in ${c.bold(rows.length)} conversation(s):\n`);
592
+ for (const r of rows) {
593
+ const date = fmtDate(r.latest_ts);
594
+ const range = r.earliest_ts && r.earliest_ts !== r.latest_ts
595
+ ? ` (also ${fmtDate(r.earliest_ts)})`
596
+ : '';
597
+ const count = r.match_count > 1 ? ` · ${r.match_count} matches` : '';
598
+ console.log(` ${c.green(date)}${c.dim(range)} ${c.dim(r.source.padEnd(14))} ${c.cyan((r.conversation_title || r.conversation_id).slice(0, 60))}${c.dim(count)}`);
599
+ }
600
+ console.log('');
601
+ console.log(c.dim(`To read one: memex get <conversation_id> | to search content: memex search "${query}"`));
602
+ }
603
+
604
+ // =============================================================
605
+ // CONTEXT — output relevant memex context for current pwd
606
+ // =============================================================
607
+ //
608
+ // Designed to be called by Claude Code SessionStart hook (or equivalent).
609
+ // stdout markdown becomes a system message injected into Claude's context
610
+ // BEFORE the user sends their first prompt. So Claude "knows" what the
611
+ // user has been doing in this project without being asked.
612
+ //
613
+ // Smart selection:
614
+ // 1. Direct project_path match — conversations where this exact path was
615
+ // captured (Claude Code/Cowork cwd, Obsidian vault, etc.)
616
+ // 2. Project-name fuzzy match — conversations whose title mentions the
617
+ // basename of pwd (catches discussions of the project across sources
618
+ // like Telegram where there's no project_path).
619
+ //
620
+ // Default budget: 1500 tokens (≈6000 chars markdown). Truncated cleanly
621
+ // if needed — never spill into Claude's context window unboundedly.
622
+ //
623
+ // Privacy: telegram source is included by default (users discussed feature
624
+ // idea here) but can be excluded via --no-source telegram. Future:
625
+ // per-source sensitivity flags in ~/.memex/config.json.
626
+ //
627
+ // Output is markdown. --json gives the structured underlying data.
628
+ async function cmdContext(args) {
629
+ const { opts } = parseArgs(args);
630
+
631
+ if (opts.help) {
632
+ console.error('Usage: memex context [--pwd PATH] [--limit N] [--budget-tokens N] [--freshness-days N] [--no-source NAME] [--json]');
633
+ console.error('');
634
+ console.error('Outputs markdown summarizing recent memex activity relevant to the current pwd.');
635
+ console.error('Designed for use as a Claude Code SessionStart hook.');
636
+ process.exit(0);
637
+ }
638
+
639
+ const pwd = opts.pwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
640
+ const limit = Math.min(20, Math.max(1, opts.limit || 5));
641
+ const tokenBudget = Math.min(8000, Math.max(50, opts.budget || 1500));
642
+ const freshnessDays = Math.min(365, Math.max(1, opts.freshnessDays || 90));
643
+ const excludeSources = Array.isArray(opts.noSource) ? opts.noSource : (opts.noSource ? [opts.noSource] : []);
644
+
645
+ const project = basename(pwd);
646
+ const sinceTs = Math.floor(Date.now() / 1000) - freshnessDays * 86400;
647
+
648
+ const db = openDb();
649
+
650
+ // 1. Direct project_path matches — highest signal.
651
+ const directFilters = [
652
+ 'project_path LIKE ?',
653
+ 'last_ts >= ?',
654
+ '(archived_at IS NULL OR archived_at = 0)',
655
+ ];
656
+ const directParams = [`%${pwd}%`, sinceTs];
657
+ if (excludeSources.length) {
658
+ const placeholders = excludeSources.map(() => '?').join(',');
659
+ directFilters.push(`source NOT IN (${placeholders})`);
660
+ directParams.push(...excludeSources);
661
+ }
662
+ const directConvs = db.prepare(`
663
+ SELECT conversation_id, source, title, first_ts, last_ts, message_count
664
+ FROM conversations
665
+ WHERE ${directFilters.join(' AND ')}
666
+ ORDER BY last_ts DESC
667
+ LIMIT ?
668
+ `).all(...directParams, limit);
669
+
670
+ // 2. Fuzzy project-name matches in title (catches Telegram / web discussion of project).
671
+ // Skip duplicates we already got from direct match.
672
+ const seenIds = new Set(directConvs.map((c) => c.conversation_id));
673
+ const fuzzyFilters = [
674
+ 'LOWER(title) LIKE LOWER(?)',
675
+ 'last_ts >= ?',
676
+ '(archived_at IS NULL OR archived_at = 0)',
677
+ ];
678
+ const fuzzyParams = [`%${project}%`, sinceTs];
679
+ if (excludeSources.length) {
680
+ const placeholders = excludeSources.map(() => '?').join(',');
681
+ fuzzyFilters.push(`source NOT IN (${placeholders})`);
682
+ fuzzyParams.push(...excludeSources);
683
+ }
684
+ const fuzzyConvs = db.prepare(`
685
+ SELECT conversation_id, source, title, first_ts, last_ts, message_count
686
+ FROM conversations
687
+ WHERE ${fuzzyFilters.join(' AND ')}
688
+ ORDER BY last_ts DESC
689
+ LIMIT ?
690
+ `).all(...fuzzyParams, limit * 2);
691
+ const filteredFuzzy = fuzzyConvs.filter((r) => !seenIds.has(r.conversation_id)).slice(0, Math.max(1, limit - directConvs.length));
692
+
693
+ db.close();
694
+
695
+ const all = [...directConvs, ...filteredFuzzy];
696
+
697
+ if (opts.json) {
698
+ console.log(JSON.stringify({
699
+ pwd, project, freshness_days: freshnessDays,
700
+ direct_matches: directConvs.length,
701
+ fuzzy_matches: filteredFuzzy.length,
702
+ conversations: all,
703
+ token_budget: tokenBudget,
704
+ }, null, 2));
705
+ return;
706
+ }
707
+
708
+ // Markdown output (TTY-color stripped — this is consumed by Claude, not the user)
709
+ const lines = [];
710
+ lines.push(`## memex auto-context for ${project}`);
711
+ lines.push('');
712
+
713
+ if (all.length === 0) {
714
+ lines.push(`_No recent activity in memex for this project (last ${freshnessDays} days)._`);
715
+ lines.push('');
716
+ lines.push(`Path searched: \`${pwd}\``);
717
+ lines.push('');
718
+ lines.push('---');
719
+ lines.push(`_memex auto-context · empty · v0.8+_`);
720
+ process.stdout.write(lines.join('\n') + '\n');
721
+ return;
722
+ }
723
+
724
+ if (directConvs.length > 0) {
725
+ lines.push(`### Recent conversations in this project (last ${freshnessDays} days)`);
726
+ lines.push('');
727
+ for (const conv of directConvs) {
728
+ const date = fmtDate(conv.last_ts);
729
+ const title = (conv.title || conv.conversation_id).slice(0, 100);
730
+ lines.push(`- **${date}** · _${conv.source}_ · ${title} (${conv.message_count} msgs)`);
731
+ }
732
+ lines.push('');
733
+ }
734
+
735
+ if (filteredFuzzy.length > 0) {
736
+ lines.push(`### Related discussions mentioning "${project}"`);
737
+ lines.push('');
738
+ for (const conv of filteredFuzzy) {
739
+ const date = fmtDate(conv.last_ts);
740
+ const title = (conv.title || conv.conversation_id).slice(0, 100);
741
+ lines.push(`- **${date}** · _${conv.source}_ · ${title}`);
742
+ }
743
+ lines.push('');
744
+ }
745
+
746
+ lines.push('---');
747
+ lines.push(`_memex auto-context · ${all.length} sources · v0.8+_`);
748
+ lines.push(`_To search deeper, ask memex (via MCP tool or terminal: \`memex search "..."\`)._`);
749
+
750
+ let out = lines.join('\n') + '\n';
751
+
752
+ // Token budget enforcement — rough estimate ≈ 4 chars/token. Truncate
753
+ // cleanly at a line boundary to keep markdown valid.
754
+ const maxChars = tokenBudget * 4;
755
+ if (out.length > maxChars) {
756
+ out = out.slice(0, maxChars);
757
+ const lastNewline = out.lastIndexOf('\n');
758
+ if (lastNewline > 0) out = out.slice(0, lastNewline);
759
+ out += '\n\n_[context truncated — exceeds token budget]_\n';
760
+ }
761
+
762
+ process.stdout.write(out);
763
+ }
764
+
765
+ // =============================================================
766
+ // HOOK — install/uninstall/status for Claude Code SessionStart
767
+ // =============================================================
768
+ async function cmdHook(args) {
769
+ const sub = args[0];
770
+ const rest = args.slice(1);
771
+ const { opts } = parseArgs(rest);
772
+
773
+ if (!sub || sub === '--help' || sub === '-h') {
774
+ console.error('Usage: memex hook <install|uninstall|status>');
775
+ console.error('');
776
+ console.error(' install Add memex SessionStart hook to ~/.claude/settings.json');
777
+ console.error(' Idempotent — re-runs are no-ops.');
778
+ console.error(' Claude Code will inject memex context on every new session.');
779
+ console.error('');
780
+ console.error(' uninstall Remove the memex hook entry. Preserves all other hooks.');
781
+ console.error('');
782
+ console.error(' status Show whether the hook is currently installed.');
783
+ process.exit(sub ? 0 : 2);
784
+ }
785
+
786
+ if (sub === 'install') {
787
+ const r = installHook();
788
+ if (opts.json) {
789
+ console.log(JSON.stringify(r, null, 2));
790
+ return;
791
+ }
792
+ if (r.error) {
793
+ console.error(`✗ ${r.error}`);
794
+ process.exit(1);
795
+ }
796
+ if (r.alreadyPresent) {
797
+ console.log(`✓ memex hook already installed in ${r.settingsPath}`);
798
+ console.log(` command: ${r.command}`);
799
+ return;
800
+ }
801
+ console.log(`✓ memex SessionStart hook installed`);
802
+ console.log(` settings: ${r.settingsPath}`);
803
+ console.log(` command: ${r.command}`);
804
+ console.log('');
805
+ console.log('Restart Claude Code (Cmd+Q + reopen) for the hook to activate.');
806
+ console.log('After restart, Claude will see memex context on every new session.');
807
+ console.log('');
808
+ console.log('Disable later: memex hook uninstall');
809
+ return;
810
+ }
811
+
812
+ if (sub === 'uninstall') {
813
+ const r = uninstallHook();
814
+ if (opts.json) {
815
+ console.log(JSON.stringify(r, null, 2));
816
+ return;
817
+ }
818
+ if (r.error) {
819
+ console.error(`✗ ${r.error}`);
820
+ process.exit(1);
821
+ }
822
+ if (!r.wasPresent) {
823
+ console.log('memex hook was not installed (nothing to remove).');
824
+ return;
825
+ }
826
+ console.log('✓ memex SessionStart hook removed');
827
+ console.log(' Other hooks in ~/.claude/settings.json preserved.');
828
+ console.log('');
829
+ console.log('Restart Claude Code (Cmd+Q + reopen) for the change to take effect.');
830
+ return;
831
+ }
832
+
833
+ if (sub === 'status') {
834
+ const r = getHookStatus();
835
+ if (opts.json) {
836
+ console.log(JSON.stringify(r, null, 2));
837
+ return;
838
+ }
839
+ console.log(`settings file: ${r.settingsPath}`);
840
+ if (!r.settingsExists) {
841
+ console.log(' status: file does not exist');
842
+ console.log(' hook: NOT installed');
843
+ return;
844
+ }
845
+ if (!r.settingsValid) {
846
+ console.log(' status: file exists but is not valid JSON');
847
+ console.log(' hook: could not determine (fix settings file first)');
848
+ return;
849
+ }
850
+ console.log(` hook: ${r.installed ? 'INSTALLED' : 'NOT installed'}`);
851
+ if (r.installed) console.log(` command: ${r.command}`);
852
+ console.log(` other SessionStart hooks: ${r.otherSessionStartHooks}`);
853
+ if (!r.installed) {
854
+ console.log('');
855
+ console.log('Install with: memex hook install');
856
+ }
857
+ return;
858
+ }
859
+
860
+ console.error(`Unknown hook subcommand: ${sub}`);
861
+ console.error('Run "memex hook --help" for usage.');
862
+ process.exit(2);
863
+ }
864
+
403
865
  // =============================================================
404
866
  // HELP — print HELP.md content
405
867
  // =============================================================
@@ -430,9 +892,16 @@ COMMANDS
430
892
  --chat "<title>" filter by conversation title (substring)
431
893
  --project <path> filter by project_path (substring)
432
894
  --sort <mode> relevance | date_asc | date_desc
895
+ --as-of YYYY-MM-DD time-travel: only messages before this date
433
896
  --limit N max results (default 10, max 50)
434
897
  --json output JSON instead of markdown
435
898
 
899
+ when "<query>" chronological "when did we talk about X" —
900
+ one row per conversation, date + title, no snippets
901
+ --source <name> filter by source
902
+ --limit N default 15, max 50
903
+ --json
904
+
436
905
  recent most recent messages across all sources
437
906
  --limit N default 20, max 100
438
907
  --source <name> filter by source
@@ -454,6 +923,21 @@ COMMANDS
454
923
  --limit N default 50, max 500
455
924
  --json
456
925
 
926
+ context output markdown summary of recent activity
927
+ in current pwd (for Claude Code SessionStart
928
+ hook — auto-injects context into new sessions)
929
+ --pwd PATH override (default: $CLAUDE_PROJECT_DIR or cwd)
930
+ --limit N max conversations to include (default 5)
931
+ --budget-tokens N cap output size (default 1500)
932
+ --freshness-days N only conversations newer than (default 90)
933
+ --no-source NAME exclude a source (repeatable; e.g. telegram)
934
+ --json
935
+
936
+ hook install install SessionStart hook in
937
+ ~/.claude/settings.json (idempotent)
938
+ hook uninstall remove only the memex hook entry
939
+ hook status show whether the hook is installed
940
+
457
941
  help print the user guide (HELP.md)
458
942
  --help, -h this command reference
459
943
  --version, -v print package version
@@ -500,6 +984,9 @@ export async function runCli(sub, args) {
500
984
  case 'get': return cmdGet(args);
501
985
  case 'overview': return cmdOverview(args);
502
986
  case 'projects': return cmdProjects(args);
987
+ case 'when': return cmdWhen(args);
988
+ case 'context': return cmdContext(args);
989
+ case 'hook': return cmdHook(args);
503
990
  case 'help': return cmdHelp();
504
991
  case '--help':
505
992
  case '-h': return cmdUsage();
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Claude Code SessionStart hook installer for memex auto-context.
3
+ *
4
+ * When the user opens a new Claude Code session, Claude Code looks at
5
+ * ~/.claude/settings.json for `hooks.SessionStart` entries and runs each
6
+ * command before showing the user the first prompt. The stdout of those
7
+ * commands gets injected into Claude's context as a system message.
8
+ *
9
+ * Memex's hook calls `memex context` which outputs a markdown summary of
10
+ * recent memex activity relevant to the current pwd. End result: Claude
11
+ * "knows" what you were doing in this project without you having to ask.
12
+ *
13
+ * Idempotency: install operations are safe to re-run. We detect our entry
14
+ * by command-string match — if any SessionStart hook command starts with
15
+ * MEMEX_COMMAND_MARKER, we treat it as ours and don't add another.
16
+ *
17
+ * Atomicity: we always write to a .tmp file first, then rename. Never
18
+ * touch the user's existing hooks for other tools.
19
+ *
20
+ * Cross-client: only Claude Code and OpenClaw have SessionStart natively.
21
+ * For Cursor/Cline/Continue/Zed, fallback strategies (MCP resource, skills,
22
+ * system prompt) are tracked separately — not part of this module.
23
+ */
24
+
25
+ import { homedir } from 'node:os';
26
+ import { join } from 'node:path';
27
+ import {
28
+ readFileSync,
29
+ writeFileSync,
30
+ renameSync,
31
+ existsSync,
32
+ mkdirSync,
33
+ } from 'node:fs';
34
+ import { execSync } from 'node:child_process';
35
+
36
+ const HOME = homedir();
37
+ const CLAUDE_DIR = join(HOME, '.claude');
38
+ const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');
39
+
40
+ // Command marker — every memex hook command starts with this. Used to
41
+ // detect our own entry for idempotency / uninstall, without collision
42
+ // risk against other tools' hooks (gstack, custom user hooks, etc.).
43
+ const MEMEX_COMMAND_MARKER = 'memex context';
44
+
45
+ /**
46
+ * Returns the absolute path to the `memex` binary that should be used in
47
+ * the hook command. Tries multiple strategies in order:
48
+ * 1. `which memex` (npm-global install)
49
+ * 2. process.execPath + this module's known location (current invocation)
50
+ * 3. fallback to bare "memex" (relies on PATH at hook execution time)
51
+ *
52
+ * Returns the resolved path string.
53
+ */
54
+ export function resolveMemexBinPath() {
55
+ try {
56
+ const which = execSync('which memex', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
57
+ if (which && existsSync(which)) return which;
58
+ } catch (_) {}
59
+ // Fallback: rely on PATH at hook-execution time. Claude Code loads
60
+ // user shell environment for hooks, so PATH usually works.
61
+ return 'memex';
62
+ }
63
+
64
+ /**
65
+ * Read ~/.claude/settings.json safely. Returns:
66
+ * { exists: bool, valid: bool, data: object, raw: string|null }
67
+ */
68
+ export function readSettings() {
69
+ if (!existsSync(SETTINGS_PATH)) {
70
+ return { exists: false, valid: true, data: {}, raw: null };
71
+ }
72
+ try {
73
+ const raw = readFileSync(SETTINGS_PATH, 'utf-8');
74
+ const data = JSON.parse(raw);
75
+ return { exists: true, valid: true, data, raw };
76
+ } catch (e) {
77
+ return {
78
+ exists: true,
79
+ valid: false,
80
+ data: {},
81
+ raw: null,
82
+ error: e.message,
83
+ };
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Find our memex SessionStart hook entry in the parsed settings.
89
+ * Returns { found: bool, index: number, command: string|null }.
90
+ *
91
+ * Claude Code's hooks schema (as of 2026):
92
+ * settings.hooks.SessionStart = [
93
+ * { matcher: "...", hooks: [{ type: "command", command: "..." }] }
94
+ * ]
95
+ *
96
+ * We treat an outer entry as "ours" if any of its inner hooks has a
97
+ * command string containing MEMEX_COMMAND_MARKER.
98
+ */
99
+ export function findMemexHookEntry(settings) {
100
+ const sessionStart = settings?.hooks?.SessionStart;
101
+ if (!Array.isArray(sessionStart)) {
102
+ return { found: false, index: -1, command: null };
103
+ }
104
+ for (let i = 0; i < sessionStart.length; i++) {
105
+ const entry = sessionStart[i];
106
+ const inner = entry?.hooks;
107
+ if (!Array.isArray(inner)) continue;
108
+ for (const h of inner) {
109
+ if (typeof h?.command === 'string' && h.command.includes(MEMEX_COMMAND_MARKER)) {
110
+ return { found: true, index: i, command: h.command };
111
+ }
112
+ }
113
+ }
114
+ return { found: false, index: -1, command: null };
115
+ }
116
+
117
+ /**
118
+ * Add the memex SessionStart hook entry to ~/.claude/settings.json.
119
+ *
120
+ * Idempotent: if a memex entry already exists, no-op (returns
121
+ * alreadyPresent: true). If the user has OTHER SessionStart hooks (e.g.
122
+ * from gstack), they are preserved untouched — we only append our entry
123
+ * to the array.
124
+ *
125
+ * Returns:
126
+ * { installed: bool, alreadyPresent: bool, settingsPath: str,
127
+ * command: str, error: str|null }
128
+ */
129
+ export function installHook(opts = {}) {
130
+ const binPath = opts.binPath || resolveMemexBinPath();
131
+ const command = `${binPath} context`;
132
+
133
+ const settings = readSettings();
134
+ if (settings.exists && !settings.valid) {
135
+ return {
136
+ installed: false,
137
+ alreadyPresent: false,
138
+ settingsPath: SETTINGS_PATH,
139
+ command,
140
+ error: `Could not parse ${SETTINGS_PATH}: ${settings.error}. Fix the file manually first.`,
141
+ };
142
+ }
143
+
144
+ const data = settings.data || {};
145
+ const existing = findMemexHookEntry(data);
146
+ if (existing.found) {
147
+ return {
148
+ installed: false,
149
+ alreadyPresent: true,
150
+ settingsPath: SETTINGS_PATH,
151
+ command: existing.command,
152
+ error: null,
153
+ };
154
+ }
155
+
156
+ // Build our entry. Use ".*" matcher (match any session) and the standard
157
+ // {type: "command", command: ...} inner hook shape.
158
+ const memexEntry = {
159
+ matcher: '.*',
160
+ hooks: [{ type: 'command', command }],
161
+ };
162
+
163
+ // Defensive nested-set: never clobber adjacent keys
164
+ if (!data.hooks) data.hooks = {};
165
+ if (!Array.isArray(data.hooks.SessionStart)) data.hooks.SessionStart = [];
166
+ data.hooks.SessionStart.push(memexEntry);
167
+
168
+ // Atomic write — temp file + rename
169
+ try {
170
+ mkdirSync(CLAUDE_DIR, { recursive: true });
171
+ const tmpPath = SETTINGS_PATH + '.tmp';
172
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
173
+ renameSync(tmpPath, SETTINGS_PATH);
174
+ } catch (e) {
175
+ return {
176
+ installed: false,
177
+ alreadyPresent: false,
178
+ settingsPath: SETTINGS_PATH,
179
+ command,
180
+ error: `Failed to write ${SETTINGS_PATH}: ${e.message}`,
181
+ };
182
+ }
183
+
184
+ return {
185
+ installed: true,
186
+ alreadyPresent: false,
187
+ settingsPath: SETTINGS_PATH,
188
+ command,
189
+ error: null,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Remove the memex SessionStart hook entry. Preserves all other hooks.
195
+ *
196
+ * Returns: { removed: bool, wasPresent: bool, error: str|null }
197
+ */
198
+ export function uninstallHook() {
199
+ const settings = readSettings();
200
+ if (!settings.exists) {
201
+ return { removed: false, wasPresent: false, error: null };
202
+ }
203
+ if (!settings.valid) {
204
+ return {
205
+ removed: false,
206
+ wasPresent: false,
207
+ error: `Could not parse ${SETTINGS_PATH}: ${settings.error}`,
208
+ };
209
+ }
210
+
211
+ const data = settings.data;
212
+ const existing = findMemexHookEntry(data);
213
+ if (!existing.found) {
214
+ return { removed: false, wasPresent: false, error: null };
215
+ }
216
+
217
+ // Remove our entry from the SessionStart array
218
+ data.hooks.SessionStart.splice(existing.index, 1);
219
+
220
+ // Cleanup empty containers — don't leave behind `hooks: {SessionStart: []}`
221
+ // detritus if memex was the only hook.
222
+ if (data.hooks.SessionStart.length === 0) delete data.hooks.SessionStart;
223
+ if (data.hooks && Object.keys(data.hooks).length === 0) delete data.hooks;
224
+
225
+ try {
226
+ const tmpPath = SETTINGS_PATH + '.tmp';
227
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
228
+ renameSync(tmpPath, SETTINGS_PATH);
229
+ } catch (e) {
230
+ return {
231
+ removed: false,
232
+ wasPresent: true,
233
+ error: `Failed to write ${SETTINGS_PATH}: ${e.message}`,
234
+ };
235
+ }
236
+
237
+ return { removed: true, wasPresent: true, error: null };
238
+ }
239
+
240
+ /**
241
+ * Inspect current hook status. Returns:
242
+ * { installed: bool, settingsPath: str, command: str|null,
243
+ * otherSessionStartHooks: number, settingsExists: bool,
244
+ * settingsValid: bool }
245
+ */
246
+ export function getHookStatus() {
247
+ const settings = readSettings();
248
+ const result = {
249
+ installed: false,
250
+ settingsPath: SETTINGS_PATH,
251
+ command: null,
252
+ otherSessionStartHooks: 0,
253
+ settingsExists: settings.exists,
254
+ settingsValid: settings.valid,
255
+ };
256
+ if (!settings.exists || !settings.valid) return result;
257
+
258
+ const sessionStart = settings.data?.hooks?.SessionStart || [];
259
+ const found = findMemexHookEntry(settings.data);
260
+ result.installed = found.found;
261
+ result.command = found.command;
262
+ result.otherSessionStartHooks = found.found
263
+ ? sessionStart.length - 1
264
+ : sessionStart.length;
265
+ return result;
266
+ }
267
+
268
+ export { MEMEX_COMMAND_MARKER, SETTINGS_PATH };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memex-mvp",
3
- "version": "0.7.0",
3
+ "version": "0.8.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 && node test/store-document.test.js && node test/cli.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 && node test/cli.test.js && node test/hook.test.js",
30
30
  "prepublishOnly": "npm test"
31
31
  },
32
32
  "engines": {
@@ -122,16 +122,35 @@ If the config file doesn't exist, create the parent directory and write a minima
122
122
 
123
123
  **Verify:** re-read the file after save; confirm `memex` entry is present and `command` is an absolute path.
124
124
 
125
- ### 3. Turn on live auto-capture
125
+ ### 3. Turn on live auto-capture + auto-context (v0.8+)
126
126
 
127
127
  ```sh
128
128
  memex-sync install
129
+ ```
130
+
131
+ During `memex-sync install`, you'll see a prompt:
132
+
133
+ ```
134
+ Auto-context (Brian Chesky mode):
135
+ When you open Claude Code in a project, memex can inject 500-1500 tokens
136
+ of relevant context so Claude knows what you were doing — without you
137
+ having to ask. Adds a SessionStart hook to ~/.claude/settings.json.
138
+ Other hooks (e.g. gstack) are preserved.
139
+
140
+ Enable? [Y/n]
141
+ ```
142
+
143
+ **Answer Y unless the user has stated privacy concerns** about Claude seeing context from their other AI conversations. Auto-context is the "10/10 magic moment" of memex — without it the install ships at base capability. Other hooks (gstack, custom) are preserved untouched.
144
+
145
+ For non-interactive flows (CI / scripts): pass `--auto-context yes` or `--yes`.
146
+
147
+ ```sh
129
148
  memex-sync status
130
149
  ```
131
150
 
132
151
  `status` should print "daemon installed", "running (PID …)", "watching N sessions".
133
152
 
134
- **Verify:** status output shows a non-zero PID.
153
+ **Verify:** status output shows a non-zero PID. Also run `memex hook status` — should show `INSTALLED` if auto-context was accepted.
135
154
 
136
155
  ### 4. Backfill existing history
137
156
 
@@ -161,6 +180,7 @@ After restart, suggest they try any of:
161
180
  - "what projects has memex captured" → triggers `memex_list_projects`
162
181
  - "search memex for [recent topic]" → triggers `memex_search`
163
182
  - "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+)
183
+ - **Open Claude Code in any project the user worked on recently** — the SessionStart auto-context (v0.8+) should kick in and Claude will mention prior work _before_ the user types anything. This is the "Brian Chesky moment" — the magical-first-impression of memex.
164
184
 
165
185
  These confirm everything works end-to-end.
166
186