memex-mvp 0.10.4 → 0.10.6

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.
@@ -213,8 +213,11 @@ function shellQuote(s) {
213
213
  *
214
214
  * opts = {
215
215
  * title, subtitle, message,
216
- * target, // 'auto' | 'claude-cli' | 'claude-desktop' | 'terminal' | 'none'
217
- * env, // optional override of detected env (for tests)
216
+ * target, // 'auto' | 'claude-cli' | 'claude-desktop' | 'terminal' | 'none'
217
+ * env, // optional override of detected env (for tests)
218
+ * dryRun, // if true → compute backend+target+command but DON'T spawn.
219
+ * // Used by unit tests so `npm test` doesn't spam real
220
+ * // macOS notifications. Also honors env MEMEX_NO_FIRE=1.
218
221
  * }
219
222
  *
220
223
  * Returns { backend: 'terminal-notifier' | 'osascript' | 'noop',
@@ -226,13 +229,14 @@ export function fireClickableNotification(opts = {}) {
226
229
 
227
230
  const target = pickTarget(opts.target || 'auto', env);
228
231
  const click = buildClickCommand(target, env);
232
+ const dryRun = opts.dryRun === true || process.env.MEMEX_NO_FIRE === '1';
229
233
 
230
234
  const title = opts.title || 'memex';
231
235
  const subtitle = opts.subtitle || '';
232
236
  const message = opts.message || '';
233
237
 
234
238
  if (env.terminal_notifier && click) {
235
- // terminal-notifier path clickable
239
+ if (dryRun) return { backend: 'terminal-notifier', target, click_command: click };
236
240
  const args = [
237
241
  '-title', title,
238
242
  '-message', message,
@@ -240,7 +244,7 @@ export function fireClickableNotification(opts = {}) {
240
244
  ];
241
245
  if (subtitle) { args.push('-subtitle'); args.push(subtitle); }
242
246
  args.push('-sound', 'Pop');
243
- args.push('-sender', 'com.apple.Terminal'); // groups under Terminal in NC
247
+ args.push('-sender', 'com.apple.Terminal');
244
248
  try {
245
249
  spawn(env.terminal_notifier, args, { detached: true, stdio: 'ignore' }).unref();
246
250
  return { backend: 'terminal-notifier', target, click_command: click };
@@ -248,6 +252,7 @@ export function fireClickableNotification(opts = {}) {
248
252
  }
249
253
 
250
254
  // Plain osascript fallback — banner is not clickable but text is informative
255
+ if (dryRun) return { backend: 'osascript', target: 'none', click_command: null };
251
256
  const esc = (s) => String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
252
257
  const sub = subtitle ? ` subtitle "${esc(subtitle)}"` : '';
253
258
  const script = `display notification "${esc(message)}" with title "${esc(title)}"${sub} sound name "Pop"`;
@@ -33,6 +33,7 @@ import {
33
33
  writeFileSync,
34
34
  renameSync,
35
35
  mkdirSync,
36
+ statSync,
36
37
  } from 'node:fs';
37
38
  import { createHash } from 'node:crypto';
38
39
  import { join, dirname } from 'node:path';
@@ -106,8 +107,26 @@ export function markCliTipShown(state, now = new Date()) {
106
107
 
107
108
  // ---------------------- Notification dedup ----------------------
108
109
 
110
+ /**
111
+ * Stable hash of a pending export for notification dedup.
112
+ *
113
+ * v0.10.5+: hash now incorporates the file's mtime in addition to path.
114
+ *
115
+ * Why: Telegram Desktop reuses the same folder name on same-day re-exports
116
+ * (e.g. ChatExport_2026-05-16). After memex imports & removes that folder
117
+ * from pending, a fresh export with the same date creates the same path
118
+ * again. Path-only hash collided → notification was incorrectly deduped
119
+ * as "already shown".
120
+ *
121
+ * Including mtime makes the hash content-aware: same path + different
122
+ * mtime → fresh hash → notification fires. If the path doesn't exist
123
+ * (file was deleted), we fall back to path-only — it's an edge case
124
+ * (notifShownFor check before fire, file should exist).
125
+ */
109
126
  export function notifIdFor(path) {
110
- return createHash('sha256').update(String(path)).digest('hex').slice(0, 16);
127
+ let mtimeKey = '';
128
+ try { mtimeKey = String(Math.floor(statSync(path).mtimeMs)); } catch (_) { /* path missing — fall back to path-only */ }
129
+ return createHash('sha256').update(String(path) + ':' + mtimeKey).digest('hex').slice(0, 16);
111
130
  }
112
131
 
113
132
  export function notifShownFor(state, path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memex-mvp",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
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",