@kamal0808/anytoany 0.1.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 (3) hide show
  1. package/README.md +92 -0
  2. package/cli.js +295 -0
  3. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # `any` — Milestone 1
2
+
3
+ A deliberately tiny message relay + CLI so two machines (each running a Claude
4
+ Code session) can **send / inbox / reply** — and, once that feels magical,
5
+ **delegate tasks**.
6
+
7
+ No auth. No websockets. No database. One ~180-line HTTP server over a JSON file,
8
+ and one CLI. Zero dependencies (Node ≥ 18 for global `fetch`; you have 25).
9
+
10
+ ```
11
+ MacBook Air (@kamal) Mac Mini (@yash)
12
+ any CLI ─────────┐ ┌─────── any CLI
13
+ ▼ ▼
14
+ relay.js on the Mac Mini (Tailscale 100.92.24.17:4747)
15
+ └─ data/messages.json
16
+ ```
17
+
18
+ ## Setup (once per machine)
19
+
20
+ Requires **Node ≥ 18** (for global `fetch`). Note the `any` shim runs whatever
21
+ `node` is first on your `PATH` — if a shell defaults to an old Node (e.g. nvm
22
+ pinned to 14), bare `any` will crash. Fix per machine with `nvm use` (an
23
+ `.nvmrc` pinning 22 is included) or, to make it stick for every shell:
24
+ `nvm alias default 22`.
25
+
26
+ From the repo root on **each** Mac:
27
+
28
+ ```bash
29
+ cd any
30
+ npm link # puts `any` on your PATH (or just use: node any/cli.js …)
31
+ ```
32
+
33
+ Identity is auto-detected from hostname (`mac-mini` → `@yash`,
34
+ `macbook-air` → `@kamal`). To be explicit:
35
+
36
+ ```bash
37
+ any register @kamal # on the MacBook Air
38
+ any register @yash # on the Mac Mini
39
+ ```
40
+
41
+ ## Run the relay (on the Mac Mini — it's always on)
42
+
43
+ ```bash
44
+ cd any
45
+ node relay.js # listens on 0.0.0.0:4747
46
+ ```
47
+
48
+ The default relay URL baked into the CLI is the Mac Mini's Tailscale IP
49
+ (`http://100.92.24.17:4747`), so both Macs already point at it. Override with:
50
+
51
+ ```bash
52
+ any relay http://100.92.24.17:4747
53
+ # or: export ANY_RELAY_URL=...
54
+ ```
55
+
56
+ Check it from either machine:
57
+
58
+ ```bash
59
+ any whoami # shows identity + "relay: online"
60
+ ```
61
+
62
+ ## The five commands
63
+
64
+ ```bash
65
+ any send @yash transcript.md # a file becomes subject + body
66
+ any send @yash "quick question…" # or send plain text
67
+ any inbox # unread messages for you
68
+ any read 3 # show #3 in full (marks it read)
69
+ any reply 3 "Looks good. Ship it." # reply goes back to the sender
70
+ any watch # live-poll the inbox (agent mode)
71
+ ```
72
+
73
+ ## The 60-second demo
74
+
75
+ 1. **Mac Mini:** `node any/relay.js` (leave it running).
76
+ 2. **MacBook Air:** `any send @yash transcript.md`
77
+ 3. **Mac Mini:** `any inbox` → `any read 1` → `any reply 1 "Looks good. Ship it."`
78
+ 4. **MacBook Air:** `any inbox` → the reply is there.
79
+
80
+ That's the illusion. If it feels magical, it's worth showing to someone.
81
+
82
+ ## Milestone 3 — delegation (the actual point)
83
+
84
+ The Mac Mini already has a Claude Code session. Paste `OPERATOR-YASH.md` into it
85
+ once. From then on it runs `any watch`, and when a task arrives it executes it in
86
+ Claude Code and replies with the result — autonomously. See that file.
87
+
88
+ ## Config & env
89
+
90
+ - Config file: `~/.any/config.json` (`{ user, relay }`)
91
+ - Env overrides: `ANY_USER`, `ANY_RELAY_URL`, `ANY_HOME`
92
+ - Relay env: `ANY_PORT`, `ANY_HOST`, `ANY_DATA_DIR`
package/cli.js ADDED
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * any — the CLI (Milestone 1)
4
+ *
5
+ * Five(ish) commands, no auth, hardcoded-friendly identity.
6
+ *
7
+ * any register @kamal set who you are (writes ~/.any/config.json)
8
+ * any relay <url> point at the relay server
9
+ * any whoami show current identity + relay
10
+ * any send @yash <text|file> send a message (file -> subject+body)
11
+ * any inbox [--all] list messages addressed to you
12
+ * any read <id> show one message (marks it read)
13
+ * any reply <id> <text> reply to a message
14
+ * any watch [--interval 5] poll inbox, print new messages as they land
15
+ *
16
+ * Config resolution (highest wins):
17
+ * env ANY_USER / ANY_RELAY_URL > ~/.any/config.json > hostname default
18
+ */
19
+
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import os from 'node:os';
23
+
24
+ const HOME = process.env.ANY_HOME || path.join(os.homedir(), '.any');
25
+ const CONFIG = path.join(HOME, 'config.json');
26
+
27
+ // Sensible defaults for this specific two-Mac setup. Override anytime with
28
+ // `any register` / `any relay`, or the ANY_USER / ANY_RELAY_URL env vars.
29
+ const DEFAULT_RELAY = 'https://anytoany-relay.kamalkhatwani69.workers.dev'; // hosted relay (Cloudflare Worker + D1)
30
+
31
+ function hostnameUser() {
32
+ const h = os.hostname().toLowerCase();
33
+ if (h.includes('mac-mini') || h.includes('macmini')) return '@yash';
34
+ if (h.includes('macbook-air') || h.includes('air')) return '@kamal';
35
+ return null;
36
+ }
37
+
38
+ function loadConfig() {
39
+ try {
40
+ return JSON.parse(fs.readFileSync(CONFIG, 'utf8'));
41
+ } catch {
42
+ return {};
43
+ }
44
+ }
45
+
46
+ function saveConfig(cfg) {
47
+ fs.mkdirSync(HOME, { recursive: true });
48
+ fs.writeFileSync(CONFIG, JSON.stringify(cfg, null, 2));
49
+ }
50
+
51
+ function resolve() {
52
+ const cfg = loadConfig();
53
+ const user = process.env.ANY_USER || cfg.user || hostnameUser();
54
+ const relay = process.env.ANY_RELAY_URL || cfg.relay || DEFAULT_RELAY;
55
+ const token = process.env.ANY_TOKEN || cfg.token || null;
56
+ return { user: norm(user), relay: relay.replace(/\/$/, ''), token };
57
+ }
58
+
59
+ function norm(user) {
60
+ if (!user) return null;
61
+ return user.startsWith('@') ? user : '@' + user;
62
+ }
63
+
64
+ function die(msg) {
65
+ console.error(`error: ${msg}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ async function api(relay, method, route, body) {
70
+ const { token } = resolve();
71
+ const headers = {};
72
+ if (body) headers['content-type'] = 'application/json';
73
+ if (token) headers['authorization'] = `Bearer ${token}`;
74
+ let res;
75
+ try {
76
+ res = await fetch(relay + route, {
77
+ method,
78
+ headers,
79
+ body: body ? JSON.stringify(body) : undefined,
80
+ });
81
+ } catch (e) {
82
+ die(`can't reach relay at ${relay} (${e.message}). Is it running? Try: any relay <url>`);
83
+ }
84
+ const text = await res.text();
85
+ let json;
86
+ try {
87
+ json = text ? JSON.parse(text) : {};
88
+ } catch {
89
+ json = { raw: text };
90
+ }
91
+ if (!res.ok) die(json.error || `relay returned ${res.status}`);
92
+ return json;
93
+ }
94
+
95
+ function fmtTime(ts) {
96
+ const d = new Date(ts);
97
+ return d.toLocaleString();
98
+ }
99
+
100
+ function printMessage(m, { full = false } = {}) {
101
+ const flag = m.read ? ' ' : '📨';
102
+ const subj = m.subject ? m.subject : '(no subject)';
103
+ console.log(`${flag} #${m.id} from ${m.from} ${fmtTime(m.ts)}`);
104
+ console.log(` ${subj}`);
105
+ if (full) {
106
+ console.log('');
107
+ console.log(
108
+ m.body
109
+ .split('\n')
110
+ .map((l) => ' ' + l)
111
+ .join('\n'),
112
+ );
113
+ } else {
114
+ const preview = m.body.replace(/\s+/g, ' ').slice(0, 72);
115
+ if (preview) console.log(` ${preview}${m.body.length > 72 ? '…' : ''}`);
116
+ }
117
+ }
118
+
119
+ // ---- commands ------------------------------------------------------------
120
+
121
+ async function cmdRegister(args) {
122
+ const user = norm(args[0]);
123
+ if (!user) die('usage: any register @you');
124
+ const cfg = loadConfig();
125
+ cfg.user = user;
126
+ saveConfig(cfg);
127
+ console.log(`registered as ${user}`);
128
+ }
129
+
130
+ async function cmdRelay(args) {
131
+ const url = args[0];
132
+ if (!url) die('usage: any relay <url>');
133
+ const cfg = loadConfig();
134
+ cfg.relay = url.replace(/\/$/, '');
135
+ saveConfig(cfg);
136
+ console.log(`relay set to ${cfg.relay}`);
137
+ }
138
+
139
+ async function cmdToken(args) {
140
+ const t = args[0];
141
+ if (!t) die('usage: any token <shared-secret>');
142
+ const cfg = loadConfig();
143
+ cfg.token = t;
144
+ saveConfig(cfg);
145
+ console.log('token saved');
146
+ }
147
+
148
+ async function cmdWhoami() {
149
+ const { user, relay, token } = resolve();
150
+ console.log(`user: ${user || '(unset — run: any register @you)'}`);
151
+ console.log(`relay: ${relay}`);
152
+ console.log(`token: ${token ? 'set' : '(none)'}`);
153
+ const health = await api(relay, 'GET', '/health').catch(() => null);
154
+ if (health) console.log(`relay: online (${health.messages} messages stored)`);
155
+ }
156
+
157
+ async function cmdSend(args) {
158
+ const { user, relay } = resolve();
159
+ if (!user) die('you are not registered. Run: any register @you');
160
+ const to = norm(args[0]);
161
+ if (!to) die('usage: any send @who <text | file>');
162
+ const rest = args.slice(1);
163
+ if (rest.length === 0) die('nothing to send');
164
+
165
+ let subject = null;
166
+ let body;
167
+ // If the single remaining arg is a readable file, send its contents.
168
+ if (rest.length === 1 && fs.existsSync(rest[0]) && fs.statSync(rest[0]).isFile()) {
169
+ body = fs.readFileSync(rest[0], 'utf8');
170
+ subject = path.basename(rest[0]);
171
+ } else {
172
+ body = rest.join(' ');
173
+ }
174
+
175
+ const { id } = await api(relay, 'POST', '/send', { from: user, to, subject, body });
176
+ console.log(`sent → ${to} (#${id})`);
177
+ }
178
+
179
+ async function cmdInbox(args) {
180
+ const { user, relay } = resolve();
181
+ if (!user) die('you are not registered. Run: any register @you');
182
+ const all = args.includes('--all');
183
+ const { messages, count } = await api(
184
+ relay,
185
+ 'GET',
186
+ `/inbox?user=${encodeURIComponent(user)}${all ? '&all=1' : ''}`,
187
+ );
188
+ if (count === 0) {
189
+ console.log(all ? 'inbox empty' : 'no new messages');
190
+ return;
191
+ }
192
+ console.log(`${count} ${all ? 'message(s)' : 'new message(s)'} for ${user}:\n`);
193
+ for (const m of messages) {
194
+ printMessage(m);
195
+ console.log('');
196
+ }
197
+ if (!all) console.log('read one with: any read <id>');
198
+ }
199
+
200
+ async function cmdRead(args) {
201
+ const { relay } = resolve();
202
+ const id = Number(args[0]);
203
+ if (!id) die('usage: any read <id>');
204
+ const m = await api(relay, 'GET', `/message/${id}`);
205
+ printMessage(m, { full: true });
206
+ await api(relay, 'POST', '/mark-read', { id });
207
+ console.log(`\nreply with: any reply ${id} "your message"`);
208
+ }
209
+
210
+ async function cmdReply(args) {
211
+ const { user, relay } = resolve();
212
+ if (!user) die('you are not registered. Run: any register @you');
213
+ const id = Number(args[0]);
214
+ if (!id) die('usage: any reply <id> <text>');
215
+ const body = args.slice(1).join(' ');
216
+ if (!body) die('nothing to reply with');
217
+ const { id: newId } = await api(relay, 'POST', '/reply', { id, from: user, body });
218
+ console.log(`replied to #${id} (#${newId})`);
219
+ }
220
+
221
+ async function cmdWatch(args) {
222
+ const { user, relay } = resolve();
223
+ if (!user) die('you are not registered. Run: any register @you');
224
+ const i = args.indexOf('--interval');
225
+ const interval = (i >= 0 ? Number(args[i + 1]) : 5) * 1000;
226
+ const seen = new Set();
227
+ console.log(`watching inbox for ${user} (every ${interval / 1000}s). Ctrl-C to stop.\n`);
228
+ // Prime with existing unread so we only announce genuinely new arrivals.
229
+ const first = await api(relay, 'GET', `/inbox?user=${encodeURIComponent(user)}&all=1`);
230
+ for (const m of first.messages) seen.add(m.id);
231
+ if (first.messages.length) console.log(`(${first.messages.length} existing message(s), waiting for new ones)\n`);
232
+
233
+ async function poll() {
234
+ const { messages } = await api(
235
+ relay,
236
+ 'GET',
237
+ `/inbox?user=${encodeURIComponent(user)}&all=1`,
238
+ ).catch(() => ({ messages: [] }));
239
+ for (const m of messages.sort((a, b) => a.ts - b.ts)) {
240
+ if (seen.has(m.id)) continue;
241
+ seen.add(m.id);
242
+ console.log('─'.repeat(50));
243
+ printMessage(m, { full: true });
244
+ console.log('─'.repeat(50) + '\n');
245
+ }
246
+ }
247
+ setInterval(poll, interval);
248
+ }
249
+
250
+ function cmdHelp() {
251
+ console.log(`any — tiny message relay CLI
252
+
253
+ any register @you set your identity
254
+ any relay <url> point at the relay server
255
+ any token <secret> set the shared secret (for a hosted relay)
256
+ any whoami show identity + relay status
257
+
258
+ any send @who <text|file> send a message (a file becomes subject+body)
259
+ any inbox [--all] list messages for you (default: unread only)
260
+ any read <id> show a message in full (marks read)
261
+ any reply <id> <text> reply to a message
262
+ any watch [--interval 5] live-poll the inbox for new messages
263
+
264
+ Config: ${CONFIG}
265
+ Env: ANY_USER, ANY_RELAY_URL, ANY_HOME
266
+ `);
267
+ }
268
+
269
+ // ---- dispatch ------------------------------------------------------------
270
+
271
+ const [, , cmd, ...args] = process.argv;
272
+
273
+ const table = {
274
+ register: cmdRegister,
275
+ relay: cmdRelay,
276
+ token: cmdToken,
277
+ whoami: cmdWhoami,
278
+ send: cmdSend,
279
+ inbox: cmdInbox,
280
+ read: cmdRead,
281
+ reply: cmdReply,
282
+ watch: cmdWatch,
283
+ help: cmdHelp,
284
+ '--help': cmdHelp,
285
+ '-h': cmdHelp,
286
+ };
287
+
288
+ const fn = table[cmd];
289
+ if (!fn) {
290
+ if (cmd) console.error(`unknown command: ${cmd}\n`);
291
+ cmdHelp();
292
+ process.exit(cmd ? 1 : 0);
293
+ }
294
+
295
+ Promise.resolve(fn(args)).catch((e) => die(e.message || String(e)));
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@kamal0808/anytoany",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Tiny CLI to send/inbox/reply messages between Claude Code sessions via the anytoany relay",
6
+ "bin": {
7
+ "any": "cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "keywords": [
16
+ "cli",
17
+ "messaging",
18
+ "claude",
19
+ "ai",
20
+ "inbox"
21
+ ],
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/kamal0808/anytoany.git"
26
+ }
27
+ }