@kamal0808/anytoany 0.2.0 → 0.3.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 +99 -77
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -55,13 +55,17 @@ function resolve() {
55
55
  const cfg = loadConfig();
56
56
  const user = process.env.ANY_USER || cfg.user || hostnameUser();
57
57
  const relay = process.env.ANY_RELAY_URL || cfg.relay || DEFAULT_RELAY;
58
- const token = process.env.ANY_TOKEN || cfg.token || null;
59
- return { user: norm(user), relay: relay.replace(/\/$/, ''), token };
58
+ const key = process.env.ANY_KEY || cfg.key || null;
59
+ return { user: norm(user), relay: relay.replace(/\/$/, ''), key };
60
60
  }
61
61
 
62
62
  function norm(user) {
63
63
  if (!user) return null;
64
- return user.startsWith('@') ? user : '@' + user;
64
+ return user.startsWith('@') ? user.toLowerCase() : '@' + user.toLowerCase();
65
+ }
66
+
67
+ function validSlug(h) {
68
+ return /^@[a-z0-9_-]{2,30}$/.test(h);
65
69
  }
66
70
 
67
71
  function die(msg) {
@@ -69,11 +73,28 @@ function die(msg) {
69
73
  process.exit(1);
70
74
  }
71
75
 
76
+ // register (claim a handle, get your own key) — no auth needed
77
+ async function registerWithRelay(relay, handle, displayName) {
78
+ let res;
79
+ try {
80
+ res = await fetch(relay + '/register', {
81
+ method: 'POST',
82
+ headers: { 'content-type': 'application/json' },
83
+ body: JSON.stringify({ handle, displayName }),
84
+ });
85
+ } catch (e) {
86
+ die(`can't reach relay at ${relay} (${e.message})`);
87
+ }
88
+ const j = await res.json().catch(() => ({}));
89
+ if (!res.ok) die(j.error || `register failed (${res.status})`);
90
+ return j; // { handle, key, displayName }
91
+ }
92
+
72
93
  async function api(relay, method, route, body) {
73
- const { token } = resolve();
94
+ const { key } = resolve();
74
95
  const headers = {};
75
96
  if (body) headers['content-type'] = 'application/json';
76
- if (token) headers['authorization'] = `Bearer ${token}`;
97
+ if (key) headers['authorization'] = `Bearer ${key}`;
77
98
  let res;
78
99
  try {
79
100
  res = await fetch(relay + route, {
@@ -122,12 +143,19 @@ function printMessage(m, { full = false } = {}) {
122
143
  // ---- commands ------------------------------------------------------------
123
144
 
124
145
  async function cmdRegister(args) {
125
- const user = norm(args[0]);
126
- if (!user) die('usage: any register @you');
146
+ let handle = norm(args[0]);
147
+ if (!handle) die('usage: any register @you [Display Name]');
148
+ if (!validSlug(handle)) die('handle must be 2-30 chars: a-z 0-9 _ - (e.g. @kamal)');
127
149
  const cfg = loadConfig();
128
- cfg.user = user;
150
+ const relay = (process.env.ANY_RELAY_URL || cfg.relay || DEFAULT_RELAY).replace(/\/$/, '');
151
+ const name = args.slice(1).join(' ') || null;
152
+ const acct = await registerWithRelay(relay, handle, name);
153
+ cfg.user = acct.handle;
154
+ cfg.key = acct.key;
155
+ cfg.relay = relay;
156
+ if (acct.displayName) cfg.displayName = acct.displayName;
129
157
  saveConfig(cfg);
130
- console.log(`registered as ${user}`);
158
+ console.log(`✓ registered as ${acct.handle}${acct.displayName ? ' (' + acct.displayName + ')' : ''}`);
131
159
  }
132
160
 
133
161
  async function cmdRelay(args) {
@@ -139,27 +167,20 @@ async function cmdRelay(args) {
139
167
  console.log(`relay set to ${cfg.relay}`);
140
168
  }
141
169
 
142
- async function cmdToken(args) {
143
- const t = args[0];
144
- if (!t) die('usage: any token <shared-secret>');
145
- const cfg = loadConfig();
146
- cfg.token = t;
147
- saveConfig(cfg);
148
- console.log('token saved');
149
- }
150
-
151
170
  async function cmdWhoami() {
152
- const { user, relay, token } = resolve();
153
- console.log(`user: ${user || '(unset — run: any register @you)'}`);
154
- console.log(`relay: ${relay}`);
155
- console.log(`token: ${token ? 'set' : '(none)'}`);
156
- const health = await api(relay, 'GET', '/health').catch(() => null);
157
- if (health) console.log(`relay: online (${health.messages} messages stored)`);
171
+ const { user, relay, key } = resolve();
172
+ console.log(`handle: ${user || '(unregistered — run: any setup)'}`);
173
+ console.log(`relay: ${relay}`);
174
+ console.log(`key: ${key ? 'set' : '(none — run: any setup)'}`);
175
+ if (key) {
176
+ const me = await api(relay, 'GET', '/whoami').catch(() => null);
177
+ if (me) console.log(`status: online as ${me.handle}${me.displayName ? ' (' + me.displayName + ')' : ''}`);
178
+ }
158
179
  }
159
180
 
160
181
  async function cmdSend(args) {
161
- const { user, relay } = resolve();
162
- if (!user) die('you are not registered. Run: any register @you');
182
+ const { key, relay } = resolve();
183
+ if (!key) die('not registered. Run: any setup');
163
184
  const to = norm(args[0]);
164
185
  if (!to) die('usage: any send @who <text | file>');
165
186
  const rest = args.slice(1);
@@ -175,24 +196,20 @@ async function cmdSend(args) {
175
196
  body = rest.join(' ');
176
197
  }
177
198
 
178
- const { id } = await api(relay, 'POST', '/send', { from: user, to, subject, body });
199
+ const { id } = await api(relay, 'POST', '/send', { to, subject, body });
179
200
  console.log(`sent → ${to} (#${id})`);
180
201
  }
181
202
 
182
203
  async function cmdInbox(args) {
183
- const { user, relay } = resolve();
184
- if (!user) die('you are not registered. Run: any register @you');
204
+ const { key, relay } = resolve();
205
+ if (!key) die('not registered. Run: any setup');
185
206
  const all = args.includes('--all');
186
- const { messages, count } = await api(
187
- relay,
188
- 'GET',
189
- `/inbox?user=${encodeURIComponent(user)}${all ? '&all=1' : ''}`,
190
- );
207
+ const { messages, count } = await api(relay, 'GET', `/inbox${all ? '?all=1' : ''}`);
191
208
  if (count === 0) {
192
209
  console.log(all ? 'inbox empty' : 'no new messages');
193
210
  return;
194
211
  }
195
- console.log(`${count} ${all ? 'message(s)' : 'new message(s)'} for ${user}:\n`);
212
+ console.log(`${count} ${all ? 'message(s)' : 'new message(s)'}:\n`);
196
213
  for (const m of messages) {
197
214
  printMessage(m);
198
215
  console.log('');
@@ -211,34 +228,30 @@ async function cmdRead(args) {
211
228
  }
212
229
 
213
230
  async function cmdReply(args) {
214
- const { user, relay } = resolve();
215
- if (!user) die('you are not registered. Run: any register @you');
231
+ const { key, relay } = resolve();
232
+ if (!key) die('not registered. Run: any setup');
216
233
  const id = Number(args[0]);
217
234
  if (!id) die('usage: any reply <id> <text>');
218
235
  const body = args.slice(1).join(' ');
219
236
  if (!body) die('nothing to reply with');
220
- const { id: newId } = await api(relay, 'POST', '/reply', { id, from: user, body });
237
+ const { id: newId } = await api(relay, 'POST', '/reply', { id, body });
221
238
  console.log(`replied to #${id} (#${newId})`);
222
239
  }
223
240
 
224
241
  async function cmdWatch(args) {
225
- const { user, relay } = resolve();
226
- if (!user) die('you are not registered. Run: any register @you');
242
+ const { key, relay } = resolve();
243
+ if (!key) die('not registered. Run: any setup');
227
244
  const i = args.indexOf('--interval');
228
245
  const interval = (i >= 0 ? Number(args[i + 1]) : 5) * 1000;
229
246
  const seen = new Set();
230
- console.log(`watching inbox for ${user} (every ${interval / 1000}s). Ctrl-C to stop.\n`);
247
+ console.log(`watching inbox (every ${interval / 1000}s). Ctrl-C to stop.\n`);
231
248
  // Prime with existing unread so we only announce genuinely new arrivals.
232
- const first = await api(relay, 'GET', `/inbox?user=${encodeURIComponent(user)}&all=1`);
249
+ const first = await api(relay, 'GET', `/inbox?all=1`);
233
250
  for (const m of first.messages) seen.add(m.id);
234
251
  if (first.messages.length) console.log(`(${first.messages.length} existing message(s), waiting for new ones)\n`);
235
252
 
236
253
  async function poll() {
237
- const { messages } = await api(
238
- relay,
239
- 'GET',
240
- `/inbox?user=${encodeURIComponent(user)}&all=1`,
241
- ).catch(() => ({ messages: [] }));
254
+ const { messages } = await api(relay, 'GET', `/inbox?all=1`).catch(() => ({ messages: [] }));
242
255
  for (const m of messages.sort((a, b) => a.ts - b.ts)) {
243
256
  if (seen.has(m.id)) continue;
244
257
  seen.add(m.id);
@@ -280,11 +293,11 @@ function fireBanner(m, link) {
280
293
 
281
294
  // one watch tick: alert on any new unread message. Run by launchd every ~15s.
282
295
  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`);
296
+ const { user, relay, key: authKey } = resolve();
297
+ if (!authKey) die('not registered (run: any setup)');
298
+ const slug = user.replace('@', '');
299
+ const seenPath = path.join(HOME, `poke-seen-${slug}.txt`);
300
+ const lockPath = path.join(HOME, `poke-${slug}.lock`);
288
301
  fs.mkdirSync(HOME, { recursive: true });
289
302
  try {
290
303
  fs.mkdirSync(lockPath); // single-flight
@@ -292,7 +305,7 @@ async function cmdPoke() {
292
305
  return;
293
306
  }
294
307
  try {
295
- const { messages } = await api(relay, 'GET', `/inbox?user=${encodeURIComponent(user)}`);
308
+ const { messages } = await api(relay, 'GET', `/inbox`);
296
309
  if (!messages || !messages.length) return;
297
310
  let seen = new Set();
298
311
  try {
@@ -331,29 +344,40 @@ function have(cmd) {
331
344
  }
332
345
  }
333
346
 
334
- // one-command onboarding: identity + token + background watcher
347
+ // one-command onboarding: claim a handle (get your key) + install the watcher
335
348
  async function cmdSetup() {
336
349
  const cfg = loadConfig();
337
350
  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: ');
351
+ const relay = (process.env.ANY_RELAY_URL || cfg.relay || DEFAULT_RELAY).replace(/\/$/, '');
352
+
353
+ if (!cfg.key) {
354
+ // claim a new account
355
+ let handle = norm(process.env.ANY_USER || cfg.user);
356
+ if (!handle) {
357
+ if (!tty) die('no handle run interactively, or: any register @you first');
358
+ handle = norm(await promptLine('Pick a handle (e.g. @yash): '));
359
+ }
360
+ if (!validSlug(handle)) die('handle must be 2-30 chars: a-z 0-9 _ - (e.g. @kamal)');
361
+ let name = cfg.displayName || process.env.ANY_NAME || null;
362
+ if (tty && !name) name = (await promptLine('Display name (optional): ')) || null;
363
+ const acct = await registerWithRelay(relay, handle, name);
364
+ cfg.user = acct.handle;
365
+ cfg.key = acct.key;
366
+ cfg.relay = relay;
367
+ if (acct.displayName) cfg.displayName = acct.displayName;
368
+ saveConfig(cfg);
369
+ console.log(`✓ registered as ${acct.handle}${acct.displayName ? ' (' + acct.displayName + ')' : ''} → ${relay}`);
370
+ } else {
371
+ if (!cfg.relay) {
372
+ cfg.relay = relay;
373
+ saveConfig(cfg);
374
+ }
375
+ console.log(`✓ already registered as ${cfg.user} → ${cfg.relay}`);
348
376
  }
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}`);
377
+ const user = cfg.user;
354
378
 
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)');
379
+ const me = await api(cfg.relay, 'GET', '/whoami').catch(() => null);
380
+ console.log(me ? `✓ connected as ${me.handle}` : '! could not verify with relay (continuing)');
357
381
 
358
382
  if (process.platform !== 'darwin') {
359
383
  console.log('\nThe background banner watcher is macOS-only for now.');
@@ -415,7 +439,7 @@ async function cmdSetup() {
415
439
 
416
440
  // prime seen-file so existing messages don't all alert at once
417
441
  try {
418
- const { messages } = await api(cfg.relay, 'GET', `/inbox?user=${encodeURIComponent(user)}&all=1`);
442
+ const { messages } = await api(cfg.relay, 'GET', `/inbox?all=1`);
419
443
  fs.writeFileSync(path.join(HOME, `poke-seen-${key}.txt`), (messages || []).map((m) => m.id).join('\n') + '\n');
420
444
  } catch {}
421
445
 
@@ -437,11 +461,10 @@ async function cmdSetup() {
437
461
  function cmdHelp() {
438
462
  console.log(`any — tiny message relay CLI
439
463
 
440
- any setup one-command onboarding (identity + token + banner watcher)
441
- any register @you set your identity
464
+ any setup one-command onboarding (claim @handle + banner watcher)
465
+ any register @you [Name] claim a handle (get your key)
442
466
  any relay <url> point at the relay server
443
- any token <secret> set the shared secret (for a hosted relay)
444
- any whoami show identity + relay status
467
+ any whoami show your handle + relay status
445
468
 
446
469
  any send @who <text|file> send a message (a file becomes subject+body)
447
470
  any inbox [--all] list messages for you (default: unread only)
@@ -463,7 +486,6 @@ const table = {
463
486
  poke: cmdPoke,
464
487
  register: cmdRegister,
465
488
  relay: cmdRelay,
466
- token: cmdToken,
467
489
  whoami: cmdWhoami,
468
490
  send: cmdSend,
469
491
  inbox: cmdInbox,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kamal0808/anytoany",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {