callwalkietalkie 0.8.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 ADDED
@@ -0,0 +1,36 @@
1
+ # callwalkietalkie
2
+
3
+ Chat into cmux on your Mac from your phone.
4
+
5
+ ```bash
6
+ npx callwalkietalkie
7
+ ```
8
+
9
+ That installs a small Python runtime into `~/.callwalkietalkie` (first run only),
10
+ starts a local server, and opens a page with a QR code. Scan it and the chat
11
+ opens on your phone — every message goes into the focused cmux terminal.
12
+
13
+ No session code to copy. No git clone. The server ships inside this package.
14
+
15
+ ## What gets installed where
16
+
17
+ | Place | What |
18
+ | --- | --- |
19
+ | npm / npx cache | This package (CLI + Python source) |
20
+ | `~/.callwalkietalkie/venv` | Durable pip environment (kept across npx runs) |
21
+
22
+ Override the home dir with `CALLWALKIETALKIE_HOME` (or legacy `LONGLEASH_HOME`) if you want.
23
+
24
+ ## Options
25
+
26
+ | Flag | Default | What |
27
+ | --- | --- | --- |
28
+ | `--port` | `8787` | Port for the local server |
29
+ | `--fresh` | off | Kill any running server and mint a new session |
30
+ | `--no-open` | off | Print the setup URL instead of opening it |
31
+
32
+ ## Notes
33
+
34
+ - macOS only, with [cmux](https://cmux.com) already running.
35
+ - Needs Python 3 on the machine for the first-run venv.
36
+ - In chat, tap **C** to show/hide command runs from the terminal.
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * npx callwalkietalkie
4
+ *
5
+ * Installs a tiny local runtime into ~/.callwalkietalkie (first run), starts the
6
+ * chat server, and opens a page with a QR code. Scan it — phone chat opens into cmux.
7
+ *
8
+ * No codes to type. No git clone required. The Python server ships inside this
9
+ * npm package; npx only caches the package, so the durable venv lives in
10
+ * ~/.callwalkietalkie (falls back to ~/.longleash if you already have one).
11
+ */
12
+
13
+ import { spawn, spawnSync } from "node:child_process";
14
+ import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { hostname, homedir, networkInterfaces, tmpdir } from "node:os";
16
+ import { dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const BOOLEAN_FLAGS = new Set(["help", "version", "no-open", "fresh"]);
20
+ const PORT = Number(process.env.CALLWALKIETALKIE_PORT || process.env.LONGLEASH_PORT || 8787);
21
+ const TOKEN =
22
+ process.env.CALLWALKIETALKIE_TOKEN || process.env.LONGLEASH_TOKEN || "agnostic-dispatch";
23
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
24
+ const BUNDLED_RUNTIME = join(PACKAGE_ROOT, "runtime");
25
+
26
+ function resolveHome() {
27
+ if (process.env.CALLWALKIETALKIE_HOME) return process.env.CALLWALKIETALKIE_HOME;
28
+ if (process.env.LONGLEASH_HOME) return process.env.LONGLEASH_HOME;
29
+ const neu = join(homedir(), ".callwalkietalkie");
30
+ const old = join(homedir(), ".longleash");
31
+ if (existsSync(join(neu, "key")) || existsSync(join(neu, "venv"))) return neu;
32
+ if (existsSync(join(old, "key")) || existsSync(join(old, "venv"))) return old;
33
+ return neu;
34
+ }
35
+
36
+ const HOME = resolveHome();
37
+
38
+ const USAGE = `Usage: npx callwalkietalkie
39
+
40
+ Starts the chat server on this Mac and opens a page with a QR code.
41
+ First run installs a small Python runtime into ~/.callwalkietalkie.
42
+
43
+ Options:
44
+ --port <PORT> port for the local server (default: ${PORT})
45
+ --fresh kill any running server and start a new session
46
+ --no-open print the URL instead of opening a browser
47
+ -h, --help
48
+ -v, --version
49
+ `;
50
+
51
+ function fail(message) {
52
+ console.error(`\n ${message}\n`);
53
+ process.exit(1);
54
+ }
55
+
56
+ function parseArgs(argv) {
57
+ const flags = {};
58
+ for (let i = 0; i < argv.length; i++) {
59
+ const arg = argv[i];
60
+ if (arg === "-h") flags.help = true;
61
+ else if (arg === "-v") flags.version = true;
62
+ else if (arg.startsWith("--")) {
63
+ const [name, inline] = arg.slice(2).split("=");
64
+ if (BOOLEAN_FLAGS.has(name) && inline === undefined) {
65
+ flags[name] = true;
66
+ continue;
67
+ }
68
+ const value = inline ?? argv[++i];
69
+ if (value === undefined) fail(`Missing value for --${name}`);
70
+ flags[name] = value;
71
+ } else if (arg === "link") {
72
+ fail("That old `link --code` flow is gone. Just run: npx callwalkietalkie");
73
+ }
74
+ }
75
+ return flags;
76
+ }
77
+
78
+ function lanAddress() {
79
+ for (const list of Object.values(networkInterfaces())) {
80
+ for (const net of list ?? []) {
81
+ if (net.family === "IPv4" && !net.internal) return net.address;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function machineName() {
88
+ return hostname().replace(/\.local$/, "");
89
+ }
90
+
91
+ async function probe(port) {
92
+ try {
93
+ const res = await fetch(
94
+ `http://127.0.0.1:${port}/setup/state?t=${encodeURIComponent(TOKEN)}`,
95
+ { signal: AbortSignal.timeout(1500) },
96
+ );
97
+ if (!res.ok) return null;
98
+ return await res.json();
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /** Where the Python server files live (shipped inside the npm package). */
105
+ function runtimeDir() {
106
+ if (existsSync(join(BUNDLED_RUNTIME, "winproxy.py"))) return BUNDLED_RUNTIME;
107
+ // Dev fallback: package linked from the git repo, runtime not synced yet.
108
+ const repo = join(PACKAGE_ROOT, "..");
109
+ if (existsSync(join(repo, "winproxy.py"))) return repo;
110
+ return null;
111
+ }
112
+
113
+ function systemPython() {
114
+ for (const cmd of ["python3", "python"]) {
115
+ const r = spawnSync(cmd, ["--version"], { encoding: "utf8" });
116
+ if (r.status === 0) return cmd;
117
+ }
118
+ return null;
119
+ }
120
+
121
+ function run(cmd, args) {
122
+ const r = spawnSync(cmd, args, { encoding: "utf8" });
123
+ if (r.status !== 0) {
124
+ const detail = (r.stderr || r.stdout || "").trim();
125
+ fail(`\`${cmd} ${args.join(" ")}\` failed${detail ? `:\n\n${detail}` : "."}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * npx unpacks this package into a cache folder under ~/.npm. That cache can be
131
+ * wiped, so the pip venv goes in ~/.callwalkietalkie instead — durable across npx runs.
132
+ */
133
+ function ensureVenv(runtime) {
134
+ const venv = join(HOME, "venv");
135
+ const python = join(venv, "bin", "python");
136
+ const reqs = join(runtime, "requirements.txt");
137
+ const stamp = join(HOME, "requirements.txt");
138
+
139
+ mkdirSync(HOME, { recursive: true });
140
+
141
+ const needCreate = !existsSync(python);
142
+ const needUpdate =
143
+ !needCreate &&
144
+ existsSync(reqs) &&
145
+ (!existsSync(stamp) || readFileSync(reqs, "utf8") !== readFileSync(stamp, "utf8"));
146
+
147
+ if (!needCreate && !needUpdate) return python;
148
+
149
+ const host = systemPython();
150
+ if (!host) {
151
+ fail("Python 3 is required. Install it from https://www.python.org/downloads/ and retry.");
152
+ }
153
+
154
+ if (needCreate) {
155
+ process.stdout.write(" Installing local runtime (one-time)… ");
156
+ run(host, ["-m", "venv", venv]);
157
+ run(python, ["-m", "pip", "install", "--upgrade", "pip"]);
158
+ } else {
159
+ process.stdout.write(" Updating local runtime… ");
160
+ }
161
+
162
+ run(python, ["-m", "pip", "install", "-r", reqs]);
163
+ writeFileSync(stamp, readFileSync(reqs, "utf8"));
164
+ console.log("done.");
165
+ return python;
166
+ }
167
+
168
+ function killExisting(port) {
169
+ try {
170
+ spawnSync("pkill", ["-f", "winproxy.py"]);
171
+ } catch {
172
+ /* ignore */
173
+ }
174
+ try {
175
+ const r = spawnSync("lsof", ["-ti", `tcp:${port}`], { encoding: "utf8" });
176
+ for (const pid of (r.stdout || "").trim().split("\n").filter(Boolean)) {
177
+ try {
178
+ process.kill(Number(pid), "SIGTERM");
179
+ } catch {
180
+ /* ignore */
181
+ }
182
+ }
183
+ } catch {
184
+ /* ignore */
185
+ }
186
+ }
187
+
188
+ async function ensureMachineKey(site) {
189
+ const keyPath = join(HOME, "key");
190
+ mkdirSync(HOME, { recursive: true });
191
+ if (existsSync(keyPath)) {
192
+ const existing = readFileSync(keyPath, "utf8").trim();
193
+ if (/^cwt_[a-f0-9]{48,128}$/i.test(existing)) return existing;
194
+ }
195
+
196
+ process.stdout.write(" Minting machine key… ");
197
+ let res;
198
+ try {
199
+ res = await fetch(`${site}/v1/keys`, {
200
+ method: "POST",
201
+ signal: AbortSignal.timeout(15000),
202
+ });
203
+ } catch (e) {
204
+ fail(`Could not reach ${site} to mint a key (${e.message || e}).`);
205
+ }
206
+ if (!res.ok) fail(`Key mint failed (${res.status}) from ${site}.`);
207
+ const body = await res.json();
208
+ const key = String(body.key || "").trim();
209
+ if (!/^cwt_[a-f0-9]{48,128}$/i.test(key)) {
210
+ fail("Relay returned an invalid machine key.");
211
+ }
212
+ writeFileSync(keyPath, `${key}\n`, { mode: 0o600 });
213
+ console.log(`saved (${body.fingerprint || "ok"}).`);
214
+ return key;
215
+ }
216
+
217
+ function start(python, runtime, port, key) {
218
+ const log = join(tmpdir(), "callwalkietalkie.log");
219
+ const out = openSync(log, "a");
220
+ spawn(python, ["winproxy.py", "--no-open"], {
221
+ cwd: runtime,
222
+ detached: true,
223
+ stdio: ["ignore", out, out],
224
+ env: {
225
+ ...process.env,
226
+ LONGLEASH_PORT: String(port),
227
+ LONGLEASH_TOKEN: TOKEN,
228
+ LONGLEASH_KEY: key,
229
+ CALLWALKIETALKIE_PORT: String(port),
230
+ CALLWALKIETALKIE_TOKEN: TOKEN,
231
+ CALLWALKIETALKIE_KEY: key,
232
+ },
233
+ }).unref();
234
+ return log;
235
+ }
236
+
237
+ async function waitUntilUp(port, seconds = 45) {
238
+ const deadline = Date.now() + seconds * 1000;
239
+ while (Date.now() < deadline) {
240
+ const state = await probe(port);
241
+ if (state?.code && state.ready !== false) return state;
242
+ await new Promise((r) => setTimeout(r, 350));
243
+ }
244
+ return null;
245
+ }
246
+
247
+ function openInBrowser(url) {
248
+ const [cmd, args] =
249
+ process.platform === "darwin"
250
+ ? ["open", [url]]
251
+ : process.platform === "win32"
252
+ ? ["cmd", ["/c", "start", "", url]]
253
+ : ["xdg-open", [url]];
254
+ try {
255
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
256
+ return true;
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
261
+
262
+ async function main() {
263
+ const flags = parseArgs(process.argv.slice(2));
264
+
265
+ if (flags.version) {
266
+ const { version } = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
267
+ console.log(version);
268
+ return;
269
+ }
270
+ if (flags.help) {
271
+ console.log(USAGE);
272
+ return;
273
+ }
274
+
275
+ if (process.platform !== "darwin") {
276
+ fail("callwalkietalkie currently needs macOS + cmux.");
277
+ }
278
+
279
+ const port = Number(flags.port || PORT);
280
+ const localSetup = `http://127.0.0.1:${port}/setup?t=${encodeURIComponent(TOKEN)}`;
281
+ const site = (
282
+ process.env.CALLWALKIETALKIE_SITE ||
283
+ process.env.LONGLEASH_SITE ||
284
+ "https://callwalkietalkie.com"
285
+ ).replace(/\/$/, "");
286
+
287
+ let state = flags.fresh ? null : await probe(port);
288
+
289
+ if (flags.fresh && (await probe(port))) {
290
+ process.stdout.write("\n Restarting… ");
291
+ killExisting(port);
292
+ await new Promise((r) => setTimeout(r, 800));
293
+ state = null;
294
+ console.log("ok.");
295
+ }
296
+
297
+ if (!state) {
298
+ const runtime = runtimeDir();
299
+ if (!runtime) {
300
+ fail(
301
+ "This package is missing its server runtime. Reinstall with: npm i -g callwalkietalkie@latest",
302
+ );
303
+ }
304
+ console.log("");
305
+ const key = await ensureMachineKey(site);
306
+ const python = ensureVenv(runtime);
307
+ process.stdout.write(" Starting… ");
308
+ const log = start(python, runtime, port, key);
309
+ state = await waitUntilUp(port);
310
+ if (!state) {
311
+ let extra = "";
312
+ try {
313
+ extra = readFileSync(log, "utf8").trim().split("\n").slice(-12).join("\n");
314
+ if (extra) extra = `\n\n${extra}`;
315
+ } catch {
316
+ /* ignore */
317
+ }
318
+ fail(`Nothing came up on port ${port}. See ${log}${extra}`);
319
+ }
320
+ console.log("ready.");
321
+ } else {
322
+ console.log("\n Already running.");
323
+ }
324
+
325
+ console.log(` Session ${state.code} on ${machineName()}.`);
326
+ if (state.fingerprint) console.log(` Key ${state.fingerprint}`);
327
+ if (!lanAddress()) {
328
+ console.log(" No wifi address found — your phone may not reach this Mac.");
329
+ }
330
+
331
+ const pairUrl =
332
+ state.pair_url || (state.code ? `${site}/pair/${state.code}` : localSetup);
333
+ if (!flags["no-open"] && openInBrowser(pairUrl)) {
334
+ console.log(" Opening callwalkietalkie.com — scan the QR with your phone.\n");
335
+ } else {
336
+ console.log(` Open this and scan the QR:\n ${pairUrl}\n`);
337
+ }
338
+ }
339
+
340
+ main();
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ /** @deprecated Use `npx callwalkietalkie` — this file remains for old installs. */
3
+ import "./callwalkietalkie.js";
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "callwalkietalkie",
3
+ "version": "0.8.0",
4
+ "description": "Chat into cmux from your phone. Run npx callwalkietalkie.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/nbened/longleash.git",
9
+ "directory": "cli"
10
+ },
11
+ "homepage": "https://callwalkietalkie.com",
12
+ "type": "module",
13
+ "bin": {
14
+ "callwalkietalkie": "bin/callwalkietalkie.js",
15
+ "longleash": "bin/callwalkietalkie.js"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "runtime",
20
+ "scripts"
21
+ ],
22
+ "scripts": {
23
+ "sync": "node scripts/sync-runtime.mjs",
24
+ "prepack": "node scripts/sync-runtime.mjs",
25
+ "prepare": "node scripts/sync-runtime.mjs"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "os": [
31
+ "darwin"
32
+ ],
33
+ "keywords": [
34
+ "callwalkietalkie",
35
+ "longleash",
36
+ "cmux",
37
+ "qr",
38
+ "pairing",
39
+ "remote",
40
+ "cli"
41
+ ]
42
+ }