parallelclaw 1.0.0
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/CHANGELOG.md +204 -0
- package/HELP.md +600 -0
- package/LICENSE +21 -0
- package/MULTI_MACHINE.md +152 -0
- package/README.md +417 -0
- package/README.ru.md +740 -0
- package/SYNC.md +844 -0
- package/bot/README.md +173 -0
- package/bot/config.js +66 -0
- package/bot/inbox.js +153 -0
- package/bot/index.js +294 -0
- package/bot/nexara.js +61 -0
- package/bot/poll.js +304 -0
- package/bot/search.js +155 -0
- package/bot/telegram.js +96 -0
- package/ingest.js +2712 -0
- package/lib/cli/index.js +1987 -0
- package/lib/config.js +220 -0
- package/lib/db-init.js +158 -0
- package/lib/hook/install.js +268 -0
- package/lib/import-telegram.js +158 -0
- package/lib/ingest-file.js +779 -0
- package/lib/notify-click-action.js +281 -0
- package/lib/openclaw-channel.js +643 -0
- package/lib/parse-cursor.js +172 -0
- package/lib/parse-obsidian.js +256 -0
- package/lib/parse-telegram-html.js +384 -0
- package/lib/parse.js +175 -0
- package/lib/render-markdown.js +0 -0
- package/lib/store-doc/canonicalize.js +116 -0
- package/lib/store-doc/detect.js +209 -0
- package/lib/store-doc/extract-title.js +162 -0
- package/lib/sync/auth.js +80 -0
- package/lib/sync/cert.js +144 -0
- package/lib/sync/cli.js +906 -0
- package/lib/sync/client.js +138 -0
- package/lib/sync/config.js +130 -0
- package/lib/sync/pair.js +145 -0
- package/lib/sync/pull.js +158 -0
- package/lib/sync/push.js +305 -0
- package/lib/sync/replicate.js +335 -0
- package/lib/sync/server.js +224 -0
- package/lib/sync/service.js +726 -0
- package/lib/tasks.js +215 -0
- package/lib/telegram-decisions.js +165 -0
- package/lib/telegram-discovery.js +373 -0
- package/lib/telegram-notify.js +272 -0
- package/lib/telegram-pending.js +200 -0
- package/lib/web/index.js +265 -0
- package/lib/web/routes/conversation.js +193 -0
- package/lib/web/routes/conversations.js +180 -0
- package/lib/web/routes/dashboard.js +175 -0
- package/lib/web/routes/pending.js +277 -0
- package/lib/web/routes/settings.js +226 -0
- package/lib/web/static/style.css +393 -0
- package/lib/web/templates.js +234 -0
- package/package.json +84 -0
- package/server.js +3816 -0
- package/skills/install-memex/README.md +109 -0
- package/skills/install-memex/SKILL.md +342 -0
- package/skills/install-memex/examples.md +294 -0
- package/skills/install-memex-claw/SKILL.md +423 -0
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
* 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.
|
|
221
|
+
* }
|
|
222
|
+
*
|
|
223
|
+
* Returns { backend: 'terminal-notifier' | 'osascript' | 'noop',
|
|
224
|
+
* target, click_command }
|
|
225
|
+
*/
|
|
226
|
+
export function fireClickableNotification(opts = {}) {
|
|
227
|
+
const env = opts.env || detectEnvironment();
|
|
228
|
+
if (env.platform !== 'darwin') return { backend: 'noop', target: 'none', click_command: null };
|
|
229
|
+
|
|
230
|
+
const target = pickTarget(opts.target || 'auto', env);
|
|
231
|
+
const click = buildClickCommand(target, env);
|
|
232
|
+
const dryRun = opts.dryRun === true || process.env.MEMEX_NO_FIRE === '1';
|
|
233
|
+
|
|
234
|
+
const title = opts.title || 'memex';
|
|
235
|
+
const subtitle = opts.subtitle || '';
|
|
236
|
+
const message = opts.message || '';
|
|
237
|
+
|
|
238
|
+
if (env.terminal_notifier && click) {
|
|
239
|
+
if (dryRun) return { backend: 'terminal-notifier', target, click_command: click };
|
|
240
|
+
const args = [
|
|
241
|
+
'-title', title,
|
|
242
|
+
'-message', message,
|
|
243
|
+
'-execute', click,
|
|
244
|
+
];
|
|
245
|
+
if (subtitle) { args.push('-subtitle'); args.push(subtitle); }
|
|
246
|
+
args.push('-sound', 'Pop');
|
|
247
|
+
args.push('-sender', 'com.apple.Terminal');
|
|
248
|
+
try {
|
|
249
|
+
spawn(env.terminal_notifier, args, { detached: true, stdio: 'ignore' }).unref();
|
|
250
|
+
return { backend: 'terminal-notifier', target, click_command: click };
|
|
251
|
+
} catch (_) { /* fall through to osascript */ }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Plain osascript fallback — banner is not clickable but text is informative
|
|
255
|
+
if (dryRun) return { backend: 'osascript', target: 'none', click_command: null };
|
|
256
|
+
const esc = (s) => String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
257
|
+
const sub = subtitle ? ` subtitle "${esc(subtitle)}"` : '';
|
|
258
|
+
const script = `display notification "${esc(message)}" with title "${esc(title)}"${sub} sound name "Pop"`;
|
|
259
|
+
try {
|
|
260
|
+
spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
261
|
+
return { backend: 'osascript', target: 'none', click_command: null };
|
|
262
|
+
} catch (_) {
|
|
263
|
+
return { backend: 'noop', target: 'none', click_command: null };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Run the click-action directly (for `memex telegram open-pending` CLI).
|
|
269
|
+
* Same target-resolution logic as the notification, just invoked from CLI.
|
|
270
|
+
*/
|
|
271
|
+
export function executeClickAction(preference = 'auto', env = detectEnvironment()) {
|
|
272
|
+
const target = pickTarget(preference, env);
|
|
273
|
+
const cmd = buildClickCommand(target, env);
|
|
274
|
+
if (!cmd) return { ran: false, target, reason: 'no-action' };
|
|
275
|
+
try {
|
|
276
|
+
spawn('sh', ['-c', cmd], { detached: true, stdio: 'ignore' }).unref();
|
|
277
|
+
return { ran: true, target, command: cmd };
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return { ran: false, target, reason: e.message };
|
|
280
|
+
}
|
|
281
|
+
}
|