memex-mvp 0.10.2 → 0.10.4
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 +3 -0
- package/README.md +1 -1
- package/ingest.js +26 -11
- package/lib/cli/index.js +105 -10
- package/lib/notify-click-action.js +276 -0
- package/lib/telegram-decisions.js +13 -1
- package/lib/telegram-notify.js +19 -0
- package/package.json +2 -2
package/HELP.md
CHANGED
|
@@ -331,6 +331,9 @@ memex telegram allow "Family" # auto-import на следу
|
|
|
331
331
|
memex telegram block "*bank*" # никогда не индексировать (glob)
|
|
332
332
|
memex telegram mode auto # pick (default) / auto / manual
|
|
333
333
|
memex telegram notifications on --show-titles # macOS notif когда детектится новый export
|
|
334
|
+
memex telegram notifications target claude-cli # клик по баннеру → откроет Claude Code CLI
|
|
335
|
+
memex telegram open-pending # открыть pending в лучшем доступном клиенте
|
|
336
|
+
memex telegram open-pending --in terminal # forcefully в Terminal с командой
|
|
334
337
|
memex telegram scan # one-shot ре-скан Downloads
|
|
335
338
|
memex telegram status # all decisions
|
|
336
339
|
|
package/README.md
CHANGED
|
@@ -206,7 +206,7 @@ Terminal equivalents: `memex telegram check / pending / import 1 3 5 / skip 2 /
|
|
|
206
206
|
|
|
207
207
|
1. **In the AI agent (active session)** — `memex_search` / `memex_recent` / `memex_overview` tool responses include a `telegram_pending` field with chat names. Agent surfaces it as a natural aside.
|
|
208
208
|
2. **In the terminal** — any `memex` CLI command appends a 💡 tip line when pending > 0. Throttled to once per 6h.
|
|
209
|
-
3. **macOS native notification** (opt-in) — daemon fires a banner when a new export is staged. `memex telegram notifications on` to enable. Default OFF for lock-screen privacy; add `--show-titles` if you want chat names in the banner.
|
|
209
|
+
3. **macOS native notification** (opt-in) — daemon fires a banner when a new export is staged. `memex telegram notifications on` to enable. Default OFF for lock-screen privacy; add `--show-titles` if you want chat names in the banner. **v0.10.4+ clickable:** if `terminal-notifier` is installed (`brew install terminal-notifier`), clicking the banner opens — in priority order — Claude Code CLI in a fresh Terminal (Brian Chesky moment via SessionStart hook), Claude Desktop, or Terminal with `memex telegram pending` queued. Override priority via `memex telegram notifications target <auto|claude-cli|claude-desktop|terminal|none>`.
|
|
210
210
|
4. **Brian Chesky hook (next Claude Code session)** — `memex context` injection includes a "🆕 N exports awaiting review" block with chat names. Claude leads with the question before you type anything.
|
|
211
211
|
|
|
212
212
|
---
|
package/ingest.js
CHANGED
|
@@ -1386,27 +1386,42 @@ function scheduleTelegramStaging(srcPath) {
|
|
|
1386
1386
|
const dest = stageExport(srcPath, { moveOrCopy: 'move' });
|
|
1387
1387
|
log(`+ telegram-export staged → pending/: ${basename(dest)}`);
|
|
1388
1388
|
|
|
1389
|
-
// Channel C: macOS native notification.
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1389
|
+
// Channel C: macOS native notification (v0.10.4+ — clickable).
|
|
1390
|
+
// Default OFF, opt-in via `memex telegram notifications on`. Dedup
|
|
1391
|
+
// by path hash so re-stages of same export don't re-notify.
|
|
1392
|
+
//
|
|
1393
|
+
// Click behavior priority (auto): Claude Code CLI → Claude Desktop
|
|
1394
|
+
// → Terminal. terminal-notifier required for click-through; without
|
|
1395
|
+
// it banner is shown via osascript (informative text, no click).
|
|
1392
1396
|
try {
|
|
1393
1397
|
const notify = await import('./lib/telegram-notify.js');
|
|
1398
|
+
const clickLib = await import('./lib/notify-click-action.js');
|
|
1394
1399
|
const state = notify.loadNotifyState();
|
|
1395
1400
|
if (state.notifications.enabled && !notify.notifShownFor(state, dest)) {
|
|
1396
|
-
// Look up preview to know the chat name (only if user opted in)
|
|
1397
1401
|
const list = listPending();
|
|
1398
1402
|
const justStaged = list.find((e) => e.path === dest) || {};
|
|
1399
1403
|
const totalPending = list.length;
|
|
1400
1404
|
const showTitles = !!state.notifications.show_titles;
|
|
1401
|
-
const
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1405
|
+
const env = clickLib.detectEnvironment();
|
|
1406
|
+
const target = clickLib.pickTarget(state.notifications.click_target || 'auto', env);
|
|
1407
|
+
const clickable = !!env.terminal_notifier && target !== 'none';
|
|
1408
|
+
const cta = clickLib.bannerCallToAction(target, clickable);
|
|
1409
|
+
|
|
1410
|
+
const title = 'memex';
|
|
1411
|
+
const subtitle = `${totalPending} new Telegram chat${totalPending === 1 ? '' : 's'} ready to review`;
|
|
1412
|
+
const message = showTitles && justStaged.chat_title
|
|
1413
|
+
? `"${justStaged.chat_title}" — ${(justStaged.message_count || 0).toLocaleString()} msgs · ${cta}`
|
|
1414
|
+
: `${cta}`;
|
|
1415
|
+
|
|
1416
|
+
const r = clickLib.fireClickableNotification({
|
|
1417
|
+
title, subtitle, message,
|
|
1418
|
+
target: state.notifications.click_target || 'auto',
|
|
1419
|
+
env,
|
|
1420
|
+
});
|
|
1421
|
+
if (r.backend !== 'noop') {
|
|
1407
1422
|
notify.markNotifShown(state, [dest]);
|
|
1408
1423
|
notify.saveNotifyState(state);
|
|
1409
|
-
log(` notif fired (
|
|
1424
|
+
log(` notif fired (${r.backend}, target=${r.target}, titles=${showTitles ? 'yes' : 'no'})`);
|
|
1410
1425
|
}
|
|
1411
1426
|
}
|
|
1412
1427
|
} catch (e) {
|
package/lib/cli/index.js
CHANGED
|
@@ -1099,8 +1099,12 @@ async function cmdTelegram(args) {
|
|
|
1099
1099
|
console.error(' mode [pick|auto|manual] Get or set capture mode (default: pick)');
|
|
1100
1100
|
console.error(' status Show counts: allowed/skipped/blocked + recent imports');
|
|
1101
1101
|
console.error(' scan One-shot rescan of ~/Downloads/Telegram Desktop/');
|
|
1102
|
-
console.error(' notifications <on|off|status> [--show-titles]');
|
|
1102
|
+
console.error(' notifications <on|off|status|target> [--show-titles]');
|
|
1103
1103
|
console.error(' macOS native notification on new export detect (default OFF)');
|
|
1104
|
+
console.error(' target <auto|claude-cli|claude-desktop|terminal|none>');
|
|
1105
|
+
console.error(' — what clicking the banner does (default auto)');
|
|
1106
|
+
console.error(' open-pending [--in <claude|claude-desktop|terminal>]');
|
|
1107
|
+
console.error(' Open pending list in best available client (Claude CLI > Desktop > Terminal)');
|
|
1104
1108
|
console.error('');
|
|
1105
1109
|
console.error(' --json Machine-readable output (works on most subcommands)');
|
|
1106
1110
|
process.exit(sub ? 0 : 2);
|
|
@@ -1125,6 +1129,7 @@ async function cmdTelegram(args) {
|
|
|
1125
1129
|
case 'status': return tgCmdStatus(opts, decisions, pending);
|
|
1126
1130
|
case 'scan': return tgCmdScan(opts, discovery, pending);
|
|
1127
1131
|
case 'notifications': return tgCmdNotifications(positionals, opts, args);
|
|
1132
|
+
case 'open-pending': return tgCmdOpenPending(positionals, opts, args);
|
|
1128
1133
|
default:
|
|
1129
1134
|
console.error(`Unknown 'memex telegram' subcommand: ${sub}`);
|
|
1130
1135
|
console.error(`Run 'memex telegram --help' for usage.`);
|
|
@@ -1506,30 +1511,68 @@ async function tgCmdNotifications(positionals, opts, args) {
|
|
|
1506
1511
|
const showTitlesFlag = args.includes('--show-titles');
|
|
1507
1512
|
const noTitlesFlag = args.includes('--no-titles') || args.includes('--hide-titles');
|
|
1508
1513
|
const notify = await import('../telegram-notify.js');
|
|
1514
|
+
const click = await import('../notify-click-action.js');
|
|
1509
1515
|
const state = notify.loadNotifyState();
|
|
1510
1516
|
|
|
1511
1517
|
if (!action || action === 'status') {
|
|
1512
|
-
|
|
1518
|
+
const env = click.detectEnvironment(true);
|
|
1519
|
+
const effectiveTarget = click.pickTarget(state.notifications.click_target, env);
|
|
1520
|
+
if (opts.json) {
|
|
1521
|
+
console.log(JSON.stringify({
|
|
1522
|
+
...state.notifications,
|
|
1523
|
+
backend: env.terminal_notifier ? 'terminal-notifier' : 'osascript',
|
|
1524
|
+
effective_target: effectiveTarget,
|
|
1525
|
+
environment: {
|
|
1526
|
+
terminal_notifier_installed: !!env.terminal_notifier,
|
|
1527
|
+
claude_cli_installed: !!env.claude_cli,
|
|
1528
|
+
claude_desktop_installed: !!env.claude_desktop,
|
|
1529
|
+
},
|
|
1530
|
+
}, null, 2));
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1513
1533
|
console.log(`Telegram notifications: ${state.notifications.enabled ? c.green('ON') : c.dim('OFF')}`);
|
|
1514
|
-
console.log(` show_titles:
|
|
1534
|
+
console.log(` show_titles: ${state.notifications.show_titles ? c.green('yes') : c.dim('no (privacy: just count)')}`);
|
|
1535
|
+
console.log(` click target: ${c.cyan(state.notifications.click_target)} → ${click.targetLabel(effectiveTarget)}`);
|
|
1536
|
+
console.log(` backend: ${env.terminal_notifier ? c.green('terminal-notifier') + c.dim(' (clickable)') : c.yellow('osascript') + c.dim(' (no click — install brew terminal-notifier for click)')}`);
|
|
1537
|
+
console.log('');
|
|
1538
|
+
console.log(` Environment:`);
|
|
1539
|
+
console.log(` terminal-notifier: ${env.terminal_notifier ? c.green('✓ ' + env.terminal_notifier) : c.dim('✗ not installed — brew install terminal-notifier')}`);
|
|
1540
|
+
console.log(` Claude Code CLI: ${env.claude_cli ? c.green('✓ ' + env.claude_cli) : c.dim('✗ not installed')}`);
|
|
1541
|
+
console.log(` Claude Desktop: ${env.claude_desktop ? c.green('✓ ' + env.claude_desktop) : c.dim('✗ not installed')}`);
|
|
1515
1542
|
console.log('');
|
|
1516
1543
|
if (!state.notifications.enabled) {
|
|
1517
1544
|
console.log('Enable: memex telegram notifications on');
|
|
1518
|
-
console.log('
|
|
1519
|
-
console.log(' ⚠ Without --show-titles, banner shows just "N new chat(s) ready to review".');
|
|
1520
|
-
console.log(' With --show-titles, banner includes chat names — visible on lock screen.');
|
|
1545
|
+
console.log(' --show-titles include chat names in banner (default off — privacy on lock screen)');
|
|
1521
1546
|
}
|
|
1547
|
+
console.log('Override click target: memex telegram notifications target <auto|claude-cli|claude-desktop|terminal|none>');
|
|
1522
1548
|
return;
|
|
1523
1549
|
}
|
|
1524
1550
|
|
|
1525
1551
|
if (action === 'on' || action === 'enable') {
|
|
1526
1552
|
notify.setNotificationsEnabled(state, true, showTitlesFlag ? true : (noTitlesFlag ? false : null));
|
|
1527
1553
|
notify.saveNotifyState(state);
|
|
1528
|
-
|
|
1554
|
+
const env = click.detectEnvironment(true);
|
|
1555
|
+
const effectiveTarget = click.pickTarget(state.notifications.click_target, env);
|
|
1556
|
+
if (opts.json) {
|
|
1557
|
+
console.log(JSON.stringify({
|
|
1558
|
+
...state.notifications,
|
|
1559
|
+
backend: env.terminal_notifier ? 'terminal-notifier' : 'osascript',
|
|
1560
|
+
effective_target: effectiveTarget,
|
|
1561
|
+
}));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1529
1564
|
console.log(`${c.green('✓')} Telegram notifications: ON`);
|
|
1530
|
-
console.log(` show_titles:
|
|
1565
|
+
console.log(` show_titles: ${state.notifications.show_titles ? 'yes' : 'no (privacy)'}`);
|
|
1566
|
+
console.log(` click target: ${effectiveTarget} (${click.targetLabel(effectiveTarget)})`);
|
|
1531
1567
|
console.log('');
|
|
1532
|
-
|
|
1568
|
+
if (!env.terminal_notifier) {
|
|
1569
|
+
console.log(c.yellow(' ℹ Banner will not be clickable — install brew terminal-notifier for click-through.'));
|
|
1570
|
+
console.log(c.dim(' Without it: banner text is self-contained ("Run: memex telegram pending").'));
|
|
1571
|
+
} else if (effectiveTarget === 'claude-cli') {
|
|
1572
|
+
console.log(c.dim(' ℹ First click → macOS will ask permission to control Terminal.'));
|
|
1573
|
+
console.log(c.dim(' Allow it — that\'s how memex opens a new Terminal tab with Claude Code.'));
|
|
1574
|
+
}
|
|
1575
|
+
console.log(c.dim(' On first export, macOS may also ask to grant notification permission.'));
|
|
1533
1576
|
return;
|
|
1534
1577
|
}
|
|
1535
1578
|
if (action === 'off' || action === 'disable') {
|
|
@@ -1539,10 +1582,62 @@ async function tgCmdNotifications(positionals, opts, args) {
|
|
|
1539
1582
|
console.log(`${c.green('✓')} Telegram notifications: OFF`);
|
|
1540
1583
|
return;
|
|
1541
1584
|
}
|
|
1542
|
-
|
|
1585
|
+
if (action === 'target') {
|
|
1586
|
+
const newTarget = positionals[1];
|
|
1587
|
+
if (!newTarget) {
|
|
1588
|
+
console.error('Usage: memex telegram notifications target <auto|claude-cli|claude-desktop|terminal|none>');
|
|
1589
|
+
process.exit(2);
|
|
1590
|
+
}
|
|
1591
|
+
try {
|
|
1592
|
+
notify.setClickTarget(state, newTarget);
|
|
1593
|
+
notify.saveNotifyState(state);
|
|
1594
|
+
} catch (e) {
|
|
1595
|
+
console.error(`✗ ${e.message}`);
|
|
1596
|
+
process.exit(2);
|
|
1597
|
+
}
|
|
1598
|
+
const env = click.detectEnvironment(true);
|
|
1599
|
+
const effectiveTarget = click.pickTarget(newTarget, env);
|
|
1600
|
+
if (opts.json) { console.log(JSON.stringify({ click_target: newTarget, effective_target: effectiveTarget })); return; }
|
|
1601
|
+
console.log(`${c.green('✓')} click target: ${newTarget} → ${click.targetLabel(effectiveTarget)}`);
|
|
1602
|
+
if (newTarget !== effectiveTarget) {
|
|
1603
|
+
console.log(c.dim(` (your preference "${newTarget}" is not installed; falling back to "${effectiveTarget}")`));
|
|
1604
|
+
}
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
console.error(`Unknown action: ${action}. Use: on | off | status | target`);
|
|
1543
1608
|
process.exit(2);
|
|
1544
1609
|
}
|
|
1545
1610
|
|
|
1611
|
+
async function tgCmdOpenPending(positionals, opts, args) {
|
|
1612
|
+
const click = await import('../notify-click-action.js');
|
|
1613
|
+
// --in <X> flag override, else 'auto'
|
|
1614
|
+
let preference = 'auto';
|
|
1615
|
+
const inIdx = args.indexOf('--in');
|
|
1616
|
+
if (inIdx >= 0 && args[inIdx + 1]) {
|
|
1617
|
+
preference = args[inIdx + 1];
|
|
1618
|
+
// Accept short aliases for ergonomics
|
|
1619
|
+
if (preference === 'claude') preference = 'claude-cli';
|
|
1620
|
+
if (preference === 'term') preference = 'terminal';
|
|
1621
|
+
if (preference === 'desktop') preference = 'claude-desktop';
|
|
1622
|
+
}
|
|
1623
|
+
const env = click.detectEnvironment(true);
|
|
1624
|
+
const target = click.pickTarget(preference, env);
|
|
1625
|
+
if (target === 'none') {
|
|
1626
|
+
console.error(`No click target available. Use --in <claude|claude-desktop|terminal>.`);
|
|
1627
|
+
process.exit(1);
|
|
1628
|
+
}
|
|
1629
|
+
if (opts.json) {
|
|
1630
|
+
console.log(JSON.stringify({ preference, effective_target: target, label: click.targetLabel(target) }));
|
|
1631
|
+
} else {
|
|
1632
|
+
console.log(`${c.cyan('▶')} Opening pending in: ${click.targetLabel(target)}`);
|
|
1633
|
+
}
|
|
1634
|
+
const r = click.executeClickAction(preference, env);
|
|
1635
|
+
if (!r.ran && !opts.json) {
|
|
1636
|
+
console.error(`✗ Failed: ${r.reason || 'unknown'}`);
|
|
1637
|
+
process.exit(1);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1546
1641
|
function tgCmdScan(opts, discovery, pending) {
|
|
1547
1642
|
const paths = discovery.defaultDownloadsPaths();
|
|
1548
1643
|
const found = discovery.discoverExports(paths);
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notification click-action picker for Telegram capture (v0.10.4+).
|
|
3
|
+
*
|
|
4
|
+
* macOS `osascript display notification` banners are NOT clickable — clicking
|
|
5
|
+
* them opens the parent app (Script Editor), which is confusing. To get a real
|
|
6
|
+
* click-action we use third-party `terminal-notifier` (brew install) which has
|
|
7
|
+
* `-execute "<shell command>"` support.
|
|
8
|
+
*
|
|
9
|
+
* Click target priority (auto-detect):
|
|
10
|
+
* 1. Claude Code CLI installed → open Terminal, launch `claude`
|
|
11
|
+
* → SessionStart hook (v0.8+) fires → agent leads with pending banner.
|
|
12
|
+
* This is the "Brian Chesky moment" — the wow case.
|
|
13
|
+
* 2. Claude Desktop installed (no CLI) → `open -a Claude`
|
|
14
|
+
* MCP is connected, but no auto-context. User has to ask.
|
|
15
|
+
* 3. Neither → open Terminal with `memex telegram pending` queued.
|
|
16
|
+
*
|
|
17
|
+
* User can override via `memex telegram notifications target <X>`:
|
|
18
|
+
* auto · claude-cli · claude-desktop · terminal · none
|
|
19
|
+
*
|
|
20
|
+
* This module is shell-out heavy; everything runs detached so we never
|
|
21
|
+
* block the daemon's chokidar event loop.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, accessSync, constants } from 'node:fs';
|
|
25
|
+
import { homedir, platform } from 'node:os';
|
|
26
|
+
import { join, delimiter } from 'node:path';
|
|
27
|
+
import { spawn } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
// ------------------------- Binary detection -------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Is a binary available on PATH? Returns the absolute path or null.
|
|
33
|
+
* We do this manually (vs `which`) so it's fast + cross-platform.
|
|
34
|
+
*/
|
|
35
|
+
export function findBin(name) {
|
|
36
|
+
const pathDirs = (process.env.PATH || '').split(delimiter).filter(Boolean);
|
|
37
|
+
// Also include common shell-rc-installed dirs that GUI daemons miss
|
|
38
|
+
const extras = [
|
|
39
|
+
join(homedir(), '.npm-global/bin'),
|
|
40
|
+
'/opt/homebrew/bin',
|
|
41
|
+
'/usr/local/bin',
|
|
42
|
+
'/usr/bin',
|
|
43
|
+
];
|
|
44
|
+
for (const dir of [...pathDirs, ...extras]) {
|
|
45
|
+
const full = join(dir, name);
|
|
46
|
+
try {
|
|
47
|
+
accessSync(full, constants.X_OK);
|
|
48
|
+
return full;
|
|
49
|
+
} catch (_) { /* not here */ }
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detect the user's notification + click-action environment.
|
|
56
|
+
*
|
|
57
|
+
* Returns an object describing what's available and what we'd pick if
|
|
58
|
+
* `target === 'auto'`. Cached briefly — detection is cheap but we don't
|
|
59
|
+
* want to fs.exists() on every fired notification.
|
|
60
|
+
*/
|
|
61
|
+
let _detectCache = null;
|
|
62
|
+
let _detectCacheAt = 0;
|
|
63
|
+
const DETECT_CACHE_MS = 30_000;
|
|
64
|
+
|
|
65
|
+
export function detectEnvironment(force = false) {
|
|
66
|
+
if (!force && _detectCache && (Date.now() - _detectCacheAt) < DETECT_CACHE_MS) {
|
|
67
|
+
return _detectCache;
|
|
68
|
+
}
|
|
69
|
+
const env = {
|
|
70
|
+
platform: platform(),
|
|
71
|
+
terminal_notifier: findBin('terminal-notifier'),
|
|
72
|
+
claude_cli: findBin('claude'),
|
|
73
|
+
claude_desktop: detectClaudeDesktop(),
|
|
74
|
+
memex_bin: findBin('memex'),
|
|
75
|
+
};
|
|
76
|
+
_detectCache = env;
|
|
77
|
+
_detectCacheAt = Date.now();
|
|
78
|
+
return env;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function detectClaudeDesktop() {
|
|
82
|
+
if (platform() !== 'darwin') return null;
|
|
83
|
+
const candidates = [
|
|
84
|
+
'/Applications/Claude.app',
|
|
85
|
+
join(homedir(), 'Applications/Claude.app'),
|
|
86
|
+
];
|
|
87
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ------------------------- Target selection -------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Decide which click-action target to use given the user's preference
|
|
95
|
+
* and detected environment.
|
|
96
|
+
*
|
|
97
|
+
* Returns one of: 'claude-cli', 'claude-desktop', 'terminal', 'none'.
|
|
98
|
+
*
|
|
99
|
+
* preference = 'auto' → priority: cli > desktop > terminal
|
|
100
|
+
* preference = 'claude-cli' → use if installed, else fall through to auto
|
|
101
|
+
* preference = 'claude-desktop' → use if installed, else fall through to auto
|
|
102
|
+
* preference = 'terminal' → always Terminal (user opted out of Claude)
|
|
103
|
+
* preference = 'none' → no click action
|
|
104
|
+
*/
|
|
105
|
+
export function pickTarget(preference, env = detectEnvironment()) {
|
|
106
|
+
if (preference === 'none') return 'none';
|
|
107
|
+
if (preference === 'terminal') return 'terminal';
|
|
108
|
+
if (preference === 'claude-cli' && env.claude_cli) return 'claude-cli';
|
|
109
|
+
if (preference === 'claude-desktop' && env.claude_desktop) return 'claude-desktop';
|
|
110
|
+
// auto (or explicit-but-not-installed) — fall through priority
|
|
111
|
+
if (env.claude_cli) return 'claude-cli';
|
|
112
|
+
if (env.claude_desktop) return 'claude-desktop';
|
|
113
|
+
return 'terminal';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Human-readable label for the chosen target — shown in `notifications status`
|
|
118
|
+
* and used as the banner-text "call to action".
|
|
119
|
+
*/
|
|
120
|
+
export function targetLabel(target) {
|
|
121
|
+
switch (target) {
|
|
122
|
+
case 'claude-cli': return 'Claude Code CLI (Brian Chesky moment)';
|
|
123
|
+
case 'claude-desktop': return 'Claude Desktop';
|
|
124
|
+
case 'terminal': return 'Terminal with `memex telegram pending`';
|
|
125
|
+
case 'none': return 'no click action';
|
|
126
|
+
default: return target;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The call-to-action shown in the banner body. Depends on:
|
|
132
|
+
* • target (claude-cli / claude-desktop / terminal / none)
|
|
133
|
+
* • clickable (is terminal-notifier installed so the banner is actually clickable?)
|
|
134
|
+
*
|
|
135
|
+
* When NOT clickable, we drop the "Click to ..." phrasing and instead
|
|
136
|
+
* show the literal shell command so users without terminal-notifier
|
|
137
|
+
* still know exactly what to do.
|
|
138
|
+
*/
|
|
139
|
+
export function bannerCallToAction(target, clickable = true) {
|
|
140
|
+
if (!clickable) {
|
|
141
|
+
// No click possible — show concrete action user must take manually
|
|
142
|
+
if (target === 'claude-cli') return 'Run: claude (or memex telegram pending)';
|
|
143
|
+
if (target === 'claude-desktop') return 'Open Claude Desktop, ask "what\'s pending in memex?"';
|
|
144
|
+
return 'Run: memex telegram pending';
|
|
145
|
+
}
|
|
146
|
+
switch (target) {
|
|
147
|
+
case 'claude-cli': return 'Click to launch Claude';
|
|
148
|
+
case 'claude-desktop': return 'Click to open Claude Desktop';
|
|
149
|
+
case 'terminal': return 'Click to open Terminal';
|
|
150
|
+
case 'none': return 'Run: memex telegram pending';
|
|
151
|
+
default: return 'memex telegram pending';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ------------------------- Build click-action shell command -------------------------
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Compose the shell command that `terminal-notifier -execute` will run when
|
|
159
|
+
* the user clicks the banner.
|
|
160
|
+
*
|
|
161
|
+
* Returns null if target = 'none' (banner has no click action).
|
|
162
|
+
*
|
|
163
|
+
* Notes:
|
|
164
|
+
* • Each target is wrapped in `osascript` to invoke macOS' Terminal app,
|
|
165
|
+
* so the user lands in an interactive shell session (not a daemon-spawned
|
|
166
|
+
* headless process).
|
|
167
|
+
* • Quoting: we shell-escape the inner double quotes for AppleScript's
|
|
168
|
+
* `do script` parameter.
|
|
169
|
+
*/
|
|
170
|
+
export function buildClickCommand(target, env = detectEnvironment()) {
|
|
171
|
+
if (target === 'none') return null;
|
|
172
|
+
|
|
173
|
+
if (target === 'claude-cli') {
|
|
174
|
+
// Open a fresh Terminal window, launch `claude` from $HOME so the
|
|
175
|
+
// SessionStart hook injects pending Telegram exports into the
|
|
176
|
+
// first message. The hook fires regardless of cwd; pending is always
|
|
177
|
+
// surfaced when count > 0.
|
|
178
|
+
const cliPath = env.claude_cli || 'claude';
|
|
179
|
+
return `osascript -e 'tell application "Terminal" to activate' ` +
|
|
180
|
+
`-e 'tell application "Terminal" to do script "cd ~ && ${escapeApple(cliPath)}"'`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (target === 'claude-desktop') {
|
|
184
|
+
const appPath = env.claude_desktop || '/Applications/Claude.app';
|
|
185
|
+
return `open ${shellQuote(appPath)}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (target === 'terminal') {
|
|
189
|
+
// Open Terminal and run `memex telegram pending` so user sees the list
|
|
190
|
+
const memexBin = env.memex_bin || 'memex';
|
|
191
|
+
return `osascript -e 'tell application "Terminal" to activate' ` +
|
|
192
|
+
`-e 'tell application "Terminal" to do script "${escapeApple(memexBin)} telegram pending"'`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// AppleScript "do script" takes a string — we need to escape backslash + dquote
|
|
199
|
+
function escapeApple(s) {
|
|
200
|
+
return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Conservative shell quote — used for `open <path>` where path may contain spaces
|
|
204
|
+
function shellQuote(s) {
|
|
205
|
+
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ------------------------- Fire the notification -------------------------
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fire a clickable notification via terminal-notifier (preferred) or fall
|
|
212
|
+
* back to plain osascript (no click).
|
|
213
|
+
*
|
|
214
|
+
* opts = {
|
|
215
|
+
* title, subtitle, message,
|
|
216
|
+
* target, // 'auto' | 'claude-cli' | 'claude-desktop' | 'terminal' | 'none'
|
|
217
|
+
* env, // optional override of detected env (for tests)
|
|
218
|
+
* }
|
|
219
|
+
*
|
|
220
|
+
* Returns { backend: 'terminal-notifier' | 'osascript' | 'noop',
|
|
221
|
+
* target, click_command }
|
|
222
|
+
*/
|
|
223
|
+
export function fireClickableNotification(opts = {}) {
|
|
224
|
+
const env = opts.env || detectEnvironment();
|
|
225
|
+
if (env.platform !== 'darwin') return { backend: 'noop', target: 'none', click_command: null };
|
|
226
|
+
|
|
227
|
+
const target = pickTarget(opts.target || 'auto', env);
|
|
228
|
+
const click = buildClickCommand(target, env);
|
|
229
|
+
|
|
230
|
+
const title = opts.title || 'memex';
|
|
231
|
+
const subtitle = opts.subtitle || '';
|
|
232
|
+
const message = opts.message || '';
|
|
233
|
+
|
|
234
|
+
if (env.terminal_notifier && click) {
|
|
235
|
+
// terminal-notifier path — clickable
|
|
236
|
+
const args = [
|
|
237
|
+
'-title', title,
|
|
238
|
+
'-message', message,
|
|
239
|
+
'-execute', click,
|
|
240
|
+
];
|
|
241
|
+
if (subtitle) { args.push('-subtitle'); args.push(subtitle); }
|
|
242
|
+
args.push('-sound', 'Pop');
|
|
243
|
+
args.push('-sender', 'com.apple.Terminal'); // groups under Terminal in NC
|
|
244
|
+
try {
|
|
245
|
+
spawn(env.terminal_notifier, args, { detached: true, stdio: 'ignore' }).unref();
|
|
246
|
+
return { backend: 'terminal-notifier', target, click_command: click };
|
|
247
|
+
} catch (_) { /* fall through to osascript */ }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Plain osascript fallback — banner is not clickable but text is informative
|
|
251
|
+
const esc = (s) => String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
252
|
+
const sub = subtitle ? ` subtitle "${esc(subtitle)}"` : '';
|
|
253
|
+
const script = `display notification "${esc(message)}" with title "${esc(title)}"${sub} sound name "Pop"`;
|
|
254
|
+
try {
|
|
255
|
+
spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
256
|
+
return { backend: 'osascript', target: 'none', click_command: null };
|
|
257
|
+
} catch (_) {
|
|
258
|
+
return { backend: 'noop', target: 'none', click_command: null };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Run the click-action directly (for `memex telegram open-pending` CLI).
|
|
264
|
+
* Same target-resolution logic as the notification, just invoked from CLI.
|
|
265
|
+
*/
|
|
266
|
+
export function executeClickAction(preference = 'auto', env = detectEnvironment()) {
|
|
267
|
+
const target = pickTarget(preference, env);
|
|
268
|
+
const cmd = buildClickCommand(target, env);
|
|
269
|
+
if (!cmd) return { ran: false, target, reason: 'no-action' };
|
|
270
|
+
try {
|
|
271
|
+
spawn('sh', ['-c', cmd], { detached: true, stdio: 'ignore' }).unref();
|
|
272
|
+
return { ran: true, target, command: cmd };
|
|
273
|
+
} catch (e) {
|
|
274
|
+
return { ran: false, target, reason: e.message };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -119,9 +119,21 @@ export function allowChat(state, title, now = new Date()) {
|
|
|
119
119
|
return state;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Skip a chat title — record a per-chat decision so future re-exports
|
|
124
|
+
* of this chat are auto-skipped.
|
|
125
|
+
*
|
|
126
|
+
* IMPORTANT: if the chat is ALREADY in allowed_chats, this is a no-op
|
|
127
|
+
* on the decisions state. The user has previously committed to indexing
|
|
128
|
+
* this chat; "skip" in this case means "throw away THIS export file"
|
|
129
|
+
* (the file removal is the caller's job), NOT "block this chat forever".
|
|
130
|
+
* If the user truly wants to stop indexing a previously-allowed chat,
|
|
131
|
+
* they should use `memex telegram remove <title>` (purges from memex.db
|
|
132
|
+
* AND marks as skipped).
|
|
133
|
+
*/
|
|
122
134
|
export function skipChat(state, title, now = new Date()) {
|
|
135
|
+
if (isAllowed(state, title)) return state; // preserve prior import decision
|
|
123
136
|
if (isSkipped(state, title)) return state;
|
|
124
|
-
state.allowed_chats = state.allowed_chats.filter((c) => norm(c.title) !== norm(title));
|
|
125
137
|
state.skipped_chats.push({ title, skipped_at: now.toISOString() });
|
|
126
138
|
return state;
|
|
127
139
|
}
|
package/lib/telegram-notify.js
CHANGED
|
@@ -49,9 +49,20 @@ const DEFAULT_STATE = () => ({
|
|
|
49
49
|
notifications: {
|
|
50
50
|
enabled: false, // privacy-first: opt-in for macOS notification
|
|
51
51
|
show_titles: false, // even when on, don't leak chat names by default
|
|
52
|
+
// v0.10.4+: which app to open when the user clicks the banner.
|
|
53
|
+
// 'auto' → priority: claude-cli > claude-desktop > terminal
|
|
54
|
+
// 'claude-cli' → force open Claude Code CLI in a new Terminal tab
|
|
55
|
+
// 'claude-desktop' → force open Claude Desktop GUI
|
|
56
|
+
// 'terminal' → force open Terminal with `memex telegram pending`
|
|
57
|
+
// 'none' → banner not clickable
|
|
58
|
+
// If terminal-notifier is not installed, click is impossible — banner
|
|
59
|
+
// text falls back to "Run: memex telegram pending".
|
|
60
|
+
click_target: 'auto',
|
|
52
61
|
},
|
|
53
62
|
});
|
|
54
63
|
|
|
64
|
+
export const VALID_CLICK_TARGETS = ['auto', 'claude-cli', 'claude-desktop', 'terminal', 'none'];
|
|
65
|
+
|
|
55
66
|
export function loadNotifyState(path = STATE_PATH) {
|
|
56
67
|
if (!existsSync(path)) return DEFAULT_STATE();
|
|
57
68
|
try {
|
|
@@ -148,6 +159,14 @@ export function setNotificationsEnabled(state, enabled, showTitles = null) {
|
|
|
148
159
|
return state;
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
export function setClickTarget(state, target) {
|
|
163
|
+
if (!VALID_CLICK_TARGETS.includes(target)) {
|
|
164
|
+
throw new Error(`Invalid click_target '${target}'. Valid: ${VALID_CLICK_TARGETS.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
state.notifications.click_target = target;
|
|
167
|
+
return state;
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
// ---------------------- Channel B formatter ----------------------
|
|
152
171
|
|
|
153
172
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memex-mvp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
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 && node test/hook.test.js && node test/telegram-html.test.js && node test/telegram-decisions.test.js && node test/telegram-pending.test.js && node test/telegram-notify.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 && node test/telegram-html.test.js && node test/telegram-decisions.test.js && node test/telegram-pending.test.js && node test/telegram-notify.test.js && node test/notify-click-action.test.js",
|
|
30
30
|
"prepublishOnly": "npm test"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|