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.
Files changed (62) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/HELP.md +600 -0
  3. package/LICENSE +21 -0
  4. package/MULTI_MACHINE.md +152 -0
  5. package/README.md +417 -0
  6. package/README.ru.md +740 -0
  7. package/SYNC.md +844 -0
  8. package/bot/README.md +173 -0
  9. package/bot/config.js +66 -0
  10. package/bot/inbox.js +153 -0
  11. package/bot/index.js +294 -0
  12. package/bot/nexara.js +61 -0
  13. package/bot/poll.js +304 -0
  14. package/bot/search.js +155 -0
  15. package/bot/telegram.js +96 -0
  16. package/ingest.js +2712 -0
  17. package/lib/cli/index.js +1987 -0
  18. package/lib/config.js +220 -0
  19. package/lib/db-init.js +158 -0
  20. package/lib/hook/install.js +268 -0
  21. package/lib/import-telegram.js +158 -0
  22. package/lib/ingest-file.js +779 -0
  23. package/lib/notify-click-action.js +281 -0
  24. package/lib/openclaw-channel.js +643 -0
  25. package/lib/parse-cursor.js +172 -0
  26. package/lib/parse-obsidian.js +256 -0
  27. package/lib/parse-telegram-html.js +384 -0
  28. package/lib/parse.js +175 -0
  29. package/lib/render-markdown.js +0 -0
  30. package/lib/store-doc/canonicalize.js +116 -0
  31. package/lib/store-doc/detect.js +209 -0
  32. package/lib/store-doc/extract-title.js +162 -0
  33. package/lib/sync/auth.js +80 -0
  34. package/lib/sync/cert.js +144 -0
  35. package/lib/sync/cli.js +906 -0
  36. package/lib/sync/client.js +138 -0
  37. package/lib/sync/config.js +130 -0
  38. package/lib/sync/pair.js +145 -0
  39. package/lib/sync/pull.js +158 -0
  40. package/lib/sync/push.js +305 -0
  41. package/lib/sync/replicate.js +335 -0
  42. package/lib/sync/server.js +224 -0
  43. package/lib/sync/service.js +726 -0
  44. package/lib/tasks.js +215 -0
  45. package/lib/telegram-decisions.js +165 -0
  46. package/lib/telegram-discovery.js +373 -0
  47. package/lib/telegram-notify.js +272 -0
  48. package/lib/telegram-pending.js +200 -0
  49. package/lib/web/index.js +265 -0
  50. package/lib/web/routes/conversation.js +193 -0
  51. package/lib/web/routes/conversations.js +180 -0
  52. package/lib/web/routes/dashboard.js +175 -0
  53. package/lib/web/routes/pending.js +277 -0
  54. package/lib/web/routes/settings.js +226 -0
  55. package/lib/web/static/style.css +393 -0
  56. package/lib/web/templates.js +234 -0
  57. package/package.json +84 -0
  58. package/server.js +3816 -0
  59. package/skills/install-memex/README.md +109 -0
  60. package/skills/install-memex/SKILL.md +342 -0
  61. package/skills/install-memex/examples.md +294 -0
  62. 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
+ }