@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.
- package/cli.js +190 -0
- 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,
|