@kamal0808/anytoany 0.1.0 β†’ 0.2.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 (2) hide show
  1. package/cli.js +190 -0
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -20,6 +20,9 @@
20
20
  import fs from 'node:fs';
21
21
  import path from 'node:path';
22
22
  import os from 'node:os';
23
+ import readline from 'node:readline';
24
+ import { execFileSync } from 'node:child_process';
25
+ import { fileURLToPath } from 'node:url';
23
26
 
24
27
  const HOME = process.env.ANY_HOME || path.join(os.homedir(), '.any');
25
28
  const CONFIG = path.join(HOME, 'config.json');
@@ -247,9 +250,194 @@ async function cmdWatch(args) {
247
250
  setInterval(poll, interval);
248
251
  }
249
252
 
253
+ // ---- watcher: poke (one tick) + setup (install the background watcher) ----
254
+
255
+ function deepLink(user, folder) {
256
+ const prompt =
257
+ `You are helping me (${user}) with my anytoany messages. Run \`any inbox\` to see new messages, ` +
258
+ `read the newest with \`any read <id>\`, and help me draft a reply. When I approve, send it with ` +
259
+ `\`any reply <id> "..."\`. Do not send anything until I confirm.`;
260
+ let url = `claude://code/new?q=${encodeURIComponent(prompt)}`;
261
+ if (folder) url += `&folder=${encodeURIComponent(folder)}`;
262
+ return url;
263
+ }
264
+
265
+ function fireBanner(m, link) {
266
+ const preview = (m.body || '').replace(/\s+/g, ' ').slice(0, 120);
267
+ const title = `πŸ“¨ ${m.from} β†’ you`;
268
+ try {
269
+ execFileSync(
270
+ 'terminal-notifier',
271
+ ['-title', title, '-subtitle', 'click to read & reply in Claude', '-message', preview, '-execute', `open '${link}'`],
272
+ { stdio: 'ignore' },
273
+ );
274
+ return;
275
+ } catch {}
276
+ try {
277
+ execFileSync('osascript', ['-e', `display notification ${JSON.stringify(preview)} with title ${JSON.stringify(title)}`], { stdio: 'ignore' });
278
+ } catch {}
279
+ }
280
+
281
+ // one watch tick: alert on any new unread message. Run by launchd every ~15s.
282
+ async function cmdPoke() {
283
+ const { user, relay } = resolve();
284
+ if (!user) die('not registered (run: any setup)');
285
+ const key = user.replace('@', '');
286
+ const seenPath = path.join(HOME, `poke-seen-${key}.txt`);
287
+ const lockPath = path.join(HOME, `poke-${key}.lock`);
288
+ fs.mkdirSync(HOME, { recursive: true });
289
+ try {
290
+ fs.mkdirSync(lockPath); // single-flight
291
+ } catch {
292
+ return;
293
+ }
294
+ try {
295
+ const { messages } = await api(relay, 'GET', `/inbox?user=${encodeURIComponent(user)}`);
296
+ if (!messages || !messages.length) return;
297
+ let seen = new Set();
298
+ try {
299
+ seen = new Set(fs.readFileSync(seenPath, 'utf8').split('\n').filter(Boolean).map(Number));
300
+ } catch {}
301
+ const link = deepLink(user, process.env.ANY_FOLDER || loadConfig().folder || null);
302
+ for (const m of messages) {
303
+ if (seen.has(m.id)) continue;
304
+ fireBanner(m, link);
305
+ fs.appendFileSync(seenPath, m.id + '\n');
306
+ console.log(`alerted #${m.id} from ${m.from}`);
307
+ }
308
+ } finally {
309
+ try {
310
+ fs.rmdirSync(lockPath);
311
+ } catch {}
312
+ }
313
+ }
314
+
315
+ function promptLine(q) {
316
+ return new Promise((res) => {
317
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
318
+ rl.question(q, (a) => {
319
+ rl.close();
320
+ res(a.trim());
321
+ });
322
+ });
323
+ }
324
+
325
+ function have(cmd) {
326
+ try {
327
+ execFileSync('/bin/bash', ['-lc', `command -v ${cmd}`], { stdio: 'ignore' });
328
+ return true;
329
+ } catch {
330
+ return false;
331
+ }
332
+ }
333
+
334
+ // one-command onboarding: identity + token + background watcher
335
+ async function cmdSetup() {
336
+ const cfg = loadConfig();
337
+ const tty = process.stdin.isTTY;
338
+
339
+ let user = norm(process.env.ANY_USER || cfg.user);
340
+ if (!user) {
341
+ if (!tty) die('no identity β€” run interactively, or: any register @you first');
342
+ user = norm(await promptLine('Your handle (e.g. @yash): '));
343
+ }
344
+ let token = process.env.ANY_TOKEN || cfg.token;
345
+ if (!token) {
346
+ if (!tty) die('no token β€” run interactively, or: any token <secret> first');
347
+ token = await promptLine('Shared token: ');
348
+ }
349
+ cfg.user = user;
350
+ cfg.token = token;
351
+ if (!cfg.relay) cfg.relay = DEFAULT_RELAY;
352
+ saveConfig(cfg);
353
+ console.log(`βœ“ configured as ${user} β†’ ${cfg.relay}`);
354
+
355
+ const health = await api(cfg.relay, 'GET', '/health').catch(() => null);
356
+ console.log(health ? 'βœ“ relay reachable' : '! could not reach relay β€” check the token/URL (continuing)');
357
+
358
+ if (process.platform !== 'darwin') {
359
+ console.log('\nThe background banner watcher is macOS-only for now.');
360
+ console.log('You can still use: any inbox / any read <id> / any reply <id> "…"');
361
+ return;
362
+ }
363
+
364
+ // ensure a stable global install (npx runs from a temp cache the watcher can't rely on)
365
+ if (!have('any')) {
366
+ console.log('Installing @kamal0808/anytoany globally…');
367
+ try {
368
+ execFileSync('npm', ['install', '-g', '@kamal0808/anytoany'], { stdio: 'inherit' });
369
+ } catch {}
370
+ }
371
+ if (!have('terminal-notifier')) {
372
+ console.log('Installing terminal-notifier (clickable banners)…');
373
+ try {
374
+ execFileSync('brew', ['install', 'terminal-notifier'], { stdio: 'inherit' });
375
+ } catch {
376
+ console.log('! could not auto-install β€” run: brew install terminal-notifier');
377
+ }
378
+ }
379
+
380
+ // resolve a stable cli.js path for the LaunchAgent
381
+ let cliForAgent = fileURLToPath(import.meta.url);
382
+ try {
383
+ const anyBin = execFileSync('/bin/bash', ['-lc', 'command -v any'], { encoding: 'utf8' }).trim();
384
+ if (anyBin) cliForAgent = fs.realpathSync(anyBin);
385
+ } catch {}
386
+
387
+ const key = user.replace('@', '');
388
+ const label = `com.anytoany.poke.${key}`;
389
+ const laDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
390
+ fs.mkdirSync(laDir, { recursive: true });
391
+ const plistPath = path.join(laDir, `${label}.plist`);
392
+ const nodeDir = path.dirname(process.execPath);
393
+ fs.writeFileSync(
394
+ plistPath,
395
+ `<?xml version="1.0" encoding="UTF-8"?>
396
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
397
+ <plist version="1.0">
398
+ <dict>
399
+ <key>Label</key><string>${label}</string>
400
+ <key>ProgramArguments</key>
401
+ <array><string>${process.execPath}</string><string>${cliForAgent}</string><string>poke</string></array>
402
+ <key>EnvironmentVariables</key>
403
+ <dict>
404
+ <key>ANY_USER</key><string>${user}</string>
405
+ <key>PATH</key><string>${nodeDir}:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
406
+ </dict>
407
+ <key>StartInterval</key><integer>15</integer>
408
+ <key>RunAtLoad</key><true/>
409
+ <key>StandardOutPath</key><string>/tmp/any-poke-${key}.log</string>
410
+ <key>StandardErrorPath</key><string>/tmp/any-poke-${key}.err</string>
411
+ </dict>
412
+ </plist>
413
+ `,
414
+ );
415
+
416
+ // prime seen-file so existing messages don't all alert at once
417
+ try {
418
+ const { messages } = await api(cfg.relay, 'GET', `/inbox?user=${encodeURIComponent(user)}&all=1`);
419
+ fs.writeFileSync(path.join(HOME, `poke-seen-${key}.txt`), (messages || []).map((m) => m.id).join('\n') + '\n');
420
+ } catch {}
421
+
422
+ const uid = process.getuid();
423
+ try {
424
+ execFileSync('launchctl', ['bootout', `gui/${uid}/${label}`], { stdio: 'ignore' });
425
+ } catch {}
426
+ try {
427
+ execFileSync('launchctl', ['bootstrap', `gui/${uid}`, plistPath], { stdio: 'ignore' });
428
+ console.log('βœ“ background watcher installed');
429
+ } catch {
430
+ console.log(`! load the watcher manually:\n launchctl bootstrap gui/${uid} ${plistPath}`);
431
+ }
432
+
433
+ console.log(`\nDone β€” you're on the network as ${user}.`);
434
+ console.log('New message β†’ a banner pops β†’ click it β†’ Claude Desktop opens to read & reply.');
435
+ }
436
+
250
437
  function cmdHelp() {
251
438
  console.log(`any β€” tiny message relay CLI
252
439
 
440
+ any setup one-command onboarding (identity + token + banner watcher)
253
441
  any register @you set your identity
254
442
  any relay <url> point at the relay server
255
443
  any token <secret> set the shared secret (for a hosted relay)
@@ -271,6 +459,8 @@ Env: ANY_USER, ANY_RELAY_URL, ANY_HOME
271
459
  const [, , cmd, ...args] = process.argv;
272
460
 
273
461
  const table = {
462
+ setup: cmdSetup,
463
+ poke: cmdPoke,
274
464
  register: cmdRegister,
275
465
  relay: cmdRelay,
276
466
  token: cmdToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kamal0808/anytoany",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Tiny CLI to send/inbox/reply messages between Claude Code sessions via the anytoany relay",
6
6
  "bin": {