@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.
- package/README.md +92 -0
- package/cli.js +295 -0
- 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
|
+
}
|