@waniwani/kit 0.1.1 → 0.1.3

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/tunnel.mjs DELETED
@@ -1,140 +0,0 @@
1
- /**
2
- * A public hostname for a dev server on this machine.
3
- *
4
- * The MCP endpoint has to be reachable from the internet before anything else
5
- * can drive it: the WaniWani chat backend runs on Vercel and cannot see
6
- * `localhost`, and neither can Claude Desktop or ChatGPT. Cloudflare answers
7
- * that with a named tunnel per agent, provisioned server-side at agent creation,
8
- * so the hostname is `<slug>.waniwani.dev` and stays that way across runs and is
9
- * safe to paste into an MCP client's config.
10
- *
11
- * The API owns the pairing: it repoints the tunnel's ingress at the port passed
12
- * to it, then mints a connector token for that one tunnel. This file runs
13
- * cloudflared against the token and reports when the edge has the connection.
14
- */
15
-
16
- import { spawn } from "node:child_process";
17
- import { existsSync } from "node:fs";
18
- import { createServer } from "node:net";
19
-
20
- const TUNNEL_READY_TIMEOUT_MS = 30_000;
21
- const SERVER_READY_TIMEOUT_MS = 30_000;
22
- const SERVER_POLL_MS = 500;
23
-
24
- /**
25
- * Bind the wildcard, matching how a Node server binds, so a listener already on
26
- * `::` or `0.0.0.0` reads as a conflict. A check against 127.0.0.1 misses those.
27
- */
28
- export function isPortAvailable(port) {
29
- return new Promise((resolve) => {
30
- const server = createServer();
31
- server.once("error", () => resolve(false));
32
- server.once("listening", () => server.close(() => resolve(true)));
33
- server.listen(port);
34
- });
35
- }
36
-
37
- /** The first free port at or above `start`. */
38
- export async function findAvailablePort(start, attempts = 20) {
39
- for (let port = start; port < start + attempts; port++) {
40
- if (await isPortAvailable(port)) return port;
41
- }
42
- throw new Error(`no free port between ${start} and ${start + attempts - 1}`);
43
- }
44
-
45
- /**
46
- * Wait until something answers on `url`.
47
- *
48
- * Any response counts, including a 404: the question is whether the dev server
49
- * has the port, and the tunnel's ingress is pointed at it before it does.
50
- */
51
- export async function waitForLocalServer(url, timeoutMs = SERVER_READY_TIMEOUT_MS) {
52
- const deadline = Date.now() + timeoutMs;
53
- while (Date.now() < deadline) {
54
- try {
55
- await fetch(url);
56
- return;
57
- } catch {
58
- await new Promise((resolve) => setTimeout(resolve, SERVER_POLL_MS));
59
- }
60
- }
61
- throw new Error(`the dev server did not answer on ${url} within ${timeoutMs / 1000}s`);
62
- }
63
-
64
- /**
65
- * Resolve the cloudflared wrapper at call time.
66
- *
67
- * It is an optional dependency, and its install step fetches a platform binary
68
- * that a CI image or a container build has no use for. A static import would
69
- * make the whole CLI fail to load wherever that install was skipped, so the cost
70
- * lands on the one command that needs it.
71
- */
72
- async function loadCloudflared() {
73
- try {
74
- return await import("cloudflared");
75
- } catch {
76
- throw new Error(
77
- "the tunnel needs the `cloudflared` package, which is not installed.\n" +
78
- " It ships as an optional dependency, so an install run with --omit=optional skips it.\n" +
79
- " Add it with `npm install cloudflared`.",
80
- );
81
- }
82
- }
83
-
84
- /** The package ships a wrapper, so the binary itself is fetched on first use. */
85
- async function cloudflaredBinary() {
86
- const { bin, install } = await loadCloudflared();
87
- if (!existsSync(bin)) await install(bin);
88
- return bin;
89
- }
90
-
91
- /**
92
- * Run the agent's named tunnel under a connector token.
93
- *
94
- * The token encodes which tunnel to serve and the ingress was set when it was
95
- * issued, so there is no `--url` to pass. Resolution waits for cloudflared to
96
- * confirm an edge connection, since anything earlier races traffic against
97
- * connector readiness.
98
- */
99
- export async function startNamedTunnel({ hostname, token }) {
100
- const bin = await cloudflaredBinary();
101
- const child = spawn(bin, ["tunnel", "--no-autoupdate", "run", "--token", token], {
102
- stdio: ["ignore", "pipe", "pipe"],
103
- });
104
-
105
- return new Promise((resolve, reject) => {
106
- let settled = false;
107
- const settle = (fn, value) => {
108
- if (settled) return;
109
- settled = true;
110
- clearTimeout(timer);
111
- fn(value);
112
- };
113
-
114
- const timer = setTimeout(() => {
115
- child.kill("SIGTERM");
116
- settle(reject, new Error(`cloudflared did not connect within ${TUNNEL_READY_TIMEOUT_MS / 1000}s`));
117
- }, TUNNEL_READY_TIMEOUT_MS);
118
-
119
- // cloudflared logs this line on each successful edge handshake, and the
120
- // first one means the hostname is serving traffic.
121
- const onOutput = (chunk) => {
122
- if (chunk.toString().includes("Registered tunnel connection")) {
123
- settle(resolve, {
124
- hostname,
125
- publicUrl: `https://${hostname}`,
126
- stop: () => {
127
- if (child.exitCode === null) child.kill("SIGTERM");
128
- },
129
- });
130
- }
131
- };
132
-
133
- child.stdout?.on("data", onOutput);
134
- child.stderr?.on("data", onOutput);
135
- child.once("error", (error) => settle(reject, error));
136
- child.once("exit", (code) => {
137
- settle(reject, new Error(`cloudflared exited with code ${code ?? "unknown"} before connecting`));
138
- });
139
- });
140
- }