agent-dir 0.4.0 → 0.5.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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **`agent-dir` turns a trusted local project directory into a controlled, authenticated MCP endpoint for AI agents.**
8
8
 
9
- `agent-dir` is a cross-platform TypeScript CLI that exposes a local project to an AI agent through an authenticated MCP server. It provides controlled file access, targeted file patches, and explicitly allowlisted development commands, with optional tunneling through [Wormhole](https://wormhole.bar/).
9
+ `agent-dir` is a cross-platform TypeScript CLI that exposes a local project to an AI agent through an authenticated MCP server. It provides controlled file access, targeted file patches, and explicitly allowlisted development commands, with optional tunneling through [Wormhole](https://wormhole.bar/) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/).
10
10
 
11
11
  > **Security:** this is a remote-control development tool. Only expose directories you trust, keep the authentication token private, and allow only commands you actually need.
12
12
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  - Node.js 24 LTS or newer
16
16
  - npm
17
- - Wormhole CLI if remote access is needed
17
+ - Wormhole CLI or `cloudflared` CLI if remote access is needed
18
18
 
19
19
  Node.js 24 is the supported LTS baseline. The project currently tracks TypeScript 7 and Biome 2.5.
20
20
 
@@ -37,6 +37,22 @@ For remote MCP access with Wormhole:
37
37
  agent-dir . --tunnel wormhole --subdomain my-project-x7k4m2
38
38
  ```
39
39
 
40
+ For a stable hostname on your own Cloudflare-managed domain, configure a Cloudflare Tunnel route to the Agent Dir port. Cloudflare Tunnel is the default for profiles that have a `tunnels.cloudflared` entry. Use `-c` to select it explicitly or `-w` to override it with Wormhole:
41
+
42
+ ```bash
43
+ agent-dir scriptr # profile default (Cloudflare when configured)
44
+ agent-dir scriptr -c # explicitly use Cloudflare Tunnel
45
+ agent-dir scriptr -w # explicitly use Wormhole
46
+ ```
47
+
48
+ For a one-off run, the equivalent long option remains available:
49
+
50
+ ```bash
51
+ agent-dir . --tunnel cloudflared --hostname mcp.example.com
52
+ ```
53
+
54
+ Agent Dir starts `cloudflared tunnel run`, monitors its connection, and restarts the process if it disconnects. The `--hostname` value is the public URL displayed by Agent Dir; Cloudflare DNS routing and the named tunnel's ingress configuration must already exist. See [Cloudflare Tunnel setup](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/).
55
+
40
56
  ### Wormhole domains
41
57
 
42
58
  When using a custom Wormhole subdomain, choose a distinctive name such as `my-project-x7k4m2`. Avoid generic names such as `project`, `test`, `dev`, or `scriptr` to reduce collisions and make the public endpoint less predictable.
@@ -49,7 +65,7 @@ The Wormhole URL is only the transport endpoint. `agent-dir` authentication is e
49
65
 
50
66
  ## First-run setup
51
67
 
52
- On the first interactive run, when no `agent-dir` config exists, the CLI walks you through creating your first saved profile. It asks for the profile name, directory, port, tunnel, optional Wormhole subdomain, optional npm scripts, optional allowed commands, and telemetry preference. The generated authentication token is saved with the profile.
68
+ On the first interactive run, when no `agent-dir` config exists, the CLI walks you through creating a saved profile. After the profile name, directory, and port, it asks whether to configure Cloudflare Tunnel first; if enabled, it collects the public hostname and Cloudflare connector token. The Cloudflare connector token is entered without being echoed to the terminal. It then asks whether to configure Wormhole as well and optionally collects a custom subdomain. You can enable either provider, both, or neither. Cloudflare is the default when both are configured. The generated Agent Dir MCP bearer token is saved separately from the Cloudflare connector token.
53
69
 
54
70
  Telemetry is optional and stays local. During setup, `agent-dir` explains the available levels: **none**, **anonymous**, **basic**, **detailed**, and **diagnostic**. Telemetry is disabled by default if you choose the default option. The same choices can later be changed with `agent-dir telemetry enable|disable`.
55
71
 
@@ -73,7 +89,7 @@ Non-interactive invocations and invocations with explicit launch options do not
73
89
 
74
90
  ## Profiles
75
91
 
76
- Profiles save settings you use repeatedly: project directory, tunnel, Wormhole subdomain, port, npm scripts, allowed non-npm commands, blocked command prefixes, and whether dedicated Git MCP tools are enabled. Dedicated Git tools are enabled automatically when `git` is in the allowed command list, or explicitly with `--git`; use `--no-git` to disable them for a run.
92
+ Profiles save settings you use repeatedly: project directory, tunnel, Wormhole subdomain or Cloudflare hostname, port, npm scripts, allowed non-npm commands, blocked command prefixes, and whether dedicated Git MCP tools are enabled. Dedicated Git tools are enabled automatically when `git` is in the allowed command list, or explicitly with `--git`; use `--no-git` to disable them for a run.
77
93
 
78
94
  ```bash
79
95
  agent-dir config add my-project \
@@ -85,10 +101,40 @@ agent-dir config add my-project \
85
101
  --blacklist "git commit,git push --force"
86
102
  ```
87
103
 
88
- Then:
104
+ Profiles can store both tunnel configurations at once. The Cloudflare connector token is separate from the Agent Dir MCP bearer token (`--token`); store it under `tunnels.cloudflared.token` or pass `--cloudflared-token` when creating the profile. Config files are stored at `~/.config/agent-dir/config.json` with owner-only permissions.
105
+
106
+ ```json
107
+ {
108
+ "directory": "~/src/my-project",
109
+ "port": 3002,
110
+ "tunnel": "cloudflared",
111
+ "defaultTunnel": "cloudflared",
112
+ "tunnels": {
113
+ "wormhole": { "subdomain": "my-project-x7k4m2" },
114
+ "cloudflared": {
115
+ "hostname": "mcp.example.com",
116
+ "token": "CLOUDFLARE_TUNNEL_TOKEN"
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ When creating a profile from the CLI, pass `--hostname` and optionally `--cloudflared-token`. **Avoid passing secrets as command-line arguments where possible**: they may be exposed in shell history or process listings. Prefer the interactive setup prompt for the connector token. To configure both providers, include both `--subdomain` and `--hostname`, and set the default using `--tunnel cloudflared` (or `-c`). `agent-dir config show` redacts configured tunnel tokens.
123
+
124
+ For Cloudflare, save the hostname instead of a Wormhole subdomain:
125
+
126
+ ```bash
127
+ agent-dir config add scriptr-mcp \
128
+ --directory ~/src/my-project \
129
+ --tunnel cloudflared \
130
+ --hostname mcp.example.com \
131
+ --npm typecheck,lint,test
132
+ ```
133
+
134
+ Then start the saved profile:
89
135
 
90
136
  ```bash
91
- agent-dir my-project
137
+ agent-dir scriptr-mcp
92
138
  ```
93
139
 
94
140
  Manage profiles with:
@@ -122,7 +168,7 @@ Profiles are stored per-user at `~/.config/agent-dir/config.json`, not in the pr
122
168
 
123
169
  ## Telemetry
124
170
 
125
- Telemetry is **disabled by default** and is local-only. When enabled, events are stored in `~/.config/agent-dir/telemetry.jsonl` with user-only permissions; no telemetry is sent to a remote service.
171
+ Telemetry is **disabled by default** and is local-only. When enabled, events are stored under `~/.config/agent-dir/telemetry/<profile>/session-<id>.jsonl` with user-only permissions. Each profile and process session gets a separate append-only JSONL file; restarting Agent Dir starts a new session file, and historical events are not truncated. Tool events also include rough byte-based token-size estimates, hashed request/input/output fingerprints, repeated-output signals, cumulative output size, and hashed per-file size metadata. These estimates do not reveal Claude's actual token billing or hidden context. File contents, source code, and raw paths are not stored in the added file metrics. No telemetry is sent to a remote service. The previous shared `telemetry.jsonl` file is migrated into the new layout on the next telemetry read/reset and preserved as a legacy archive.
126
172
 
127
173
  Enable a privacy level with:
128
174
 
@@ -146,13 +192,14 @@ Manage telemetry with:
146
192
  ```bash
147
193
  agent-dir telemetry status
148
194
  agent-dir telemetry schema
149
- agent-dir telemetry show [--follow]
150
- agent-dir telemetry summary
195
+ agent-dir telemetry show [profile] [session-id] [--follow]
196
+ agent-dir telemetry summary [profile]
151
197
  agent-dir telemetry disable
152
- agent-dir telemetry reset
198
+ agent-dir telemetry reset <profile>
199
+ agent-dir telemetry reset --all
153
200
  ```
154
201
 
155
- For the event model, metric definitions, privacy levels, and guidance for interpreting raw and aggregated telemetry, see [Telemetry interpretation](docs/telemetry.md).
202
+ Use `agent-dir telemetry show scriptr` or `agent-dir telemetry summary scriptr` to focus on one profile. `agent-dir telemetry show scriptr <session-id>` filters to a single session. `agent-dir telemetry reset scriptr` removes only that profile’s telemetry; `agent-dir telemetry reset --all` clears all telemetry and resets the configured level. For the event model, metric definitions, privacy levels, and guidance for interpreting raw and aggregated telemetry, see [Telemetry interpretation](docs/telemetry.md).
156
203
 
157
204
  ## MCP transport and compatibility
158
205
 
@@ -426,7 +473,7 @@ The diagnostics tool is deliberately project-independent. It checks conditions s
426
473
 
427
474
  ## Credits
428
475
 
429
- Remote tunneling support is provided through [Wormhole](https://wormhole.bar/), a project of the Wormhole team. `agent-dir` invokes the Wormhole CLI and does not provide the tunneling service itself.
476
+ Remote tunneling is provided by [Wormhole](https://wormhole.bar/) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/). `agent-dir` invokes the installed CLI and does not provide either tunneling service itself. Cloudflare support expects an already-configured named tunnel.
430
477
 
431
478
  ## License
432
479
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createHash, randomBytes } from "node:crypto";
3
- import { mkdirSync, readFileSync, unlinkSync, watch, writeFileSync } from "node:fs";
2
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
3
+ import { watch } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
5
  import { resolve } from "node:path";
6
6
  import { createInterface } from "node:readline/promises";
@@ -12,6 +12,7 @@ import { hasConfigFile, runFirstTimeSetup, shouldOfferSetup } from "../src/setup
12
12
  import { summarizeTelemetry } from "../src/telemetry/aggregate.js";
13
13
  import { isTelemetryLevel } from "../src/telemetry/config.js";
14
14
  import { formatTelemetrySummary } from "../src/telemetry/format.js";
15
+ import { ensureTelemetryDirectory, migrateLegacyTelemetry, readAllTelemetryEvents, resetTelemetry, TELEMETRY_DIRECTORY, } from "../src/telemetry/storage.js";
15
16
  import { startTunnel } from "../src/tunnel.js";
16
17
  const require = createRequire(import.meta.url);
17
18
  const packageVersion = require("../../package.json").version;
@@ -41,7 +42,11 @@ const { values, positionals } = parseArgs({
41
42
  directory: { type: "string", short: "d" },
42
43
  port: { type: "string", short: "p" },
43
44
  tunnel: { type: "string", short: "t" },
45
+ wormhole: { type: "boolean", short: "w" },
46
+ cloudflared: { type: "boolean", short: "c" },
44
47
  subdomain: { type: "string", short: "s" },
48
+ hostname: { type: "string" },
49
+ "cloudflared-token": { type: "string" },
45
50
  npm: { type: "string" },
46
51
  command: { type: "string" },
47
52
  blacklist: { type: "string" },
@@ -75,7 +80,8 @@ if (command === "setup") {
75
80
  if (shouldOfferSetup(await hasConfigFile(), {
76
81
  random: values.random,
77
82
  noTunnel: values["no-tunnel"],
78
- tunnel: values.tunnel,
83
+ tunnel: values.tunnel ??
84
+ (values.wormhole ? "wormhole" : values.cloudflared ? "cloudflared" : undefined),
79
85
  }) &&
80
86
  command !== "config" &&
81
87
  command !== "telemetry") {
@@ -90,7 +96,8 @@ if (shouldOfferSetup(await hasConfigFile(), {
90
96
  if (command === "telemetry") {
91
97
  const action = positionals[1];
92
98
  const currentLevel = config.telemetry?.level ?? "none";
93
- const telemetryFile = resolve(`${process.env.HOME ?? "."}/.config/agent-dir/telemetry.jsonl`);
99
+ const telemetryProfile = positionals[2];
100
+ const telemetrySession = positionals[3];
94
101
  if (action === "status") {
95
102
  console.log(`Telemetry: ${currentLevel === "none" ? "disabled" : "enabled"}`);
96
103
  console.log(`Level: ${currentLevel}`);
@@ -138,12 +145,13 @@ if (command === "telemetry") {
138
145
  process.exit(0);
139
146
  }
140
147
  if (action === "summary") {
141
- console.log(formatTelemetrySummary(summarizeTelemetry(readTelemetryEvents(telemetryFile)), currentLevel));
148
+ console.log(formatTelemetrySummary(summarizeTelemetry(readAllTelemetryEvents().filter((event) => !telemetryProfile || event.configId === telemetryProfile)), currentLevel));
142
149
  process.exit(0);
143
150
  }
144
151
  if (action === "show") {
145
152
  const printEvents = () => {
146
- const events = readTelemetryEvents(telemetryFile);
153
+ const events = readAllTelemetryEvents().filter((event) => (!telemetryProfile || event.configId === telemetryProfile) &&
154
+ (!telemetrySession || event.sessionId === telemetrySession));
147
155
  const grouped = new Map();
148
156
  for (const event of events) {
149
157
  const configId = isRecord(event) && typeof event.configId === "string" ? event.configId : "unknown";
@@ -170,17 +178,8 @@ if (command === "telemetry") {
170
178
  };
171
179
  printEvents();
172
180
  if (values.follow) {
173
- mkdirSync(resolve(`${process.env.HOME ?? "."}/.config/agent-dir`), {
174
- recursive: true,
175
- mode: 0o700,
176
- });
177
- try {
178
- readFileSync(telemetryFile);
179
- }
180
- catch {
181
- writeFileSync(telemetryFile, "", { encoding: "utf8", mode: 0o600 });
182
- }
183
- const watcher = watch(telemetryFile, () => {
181
+ ensureTelemetryDirectory();
182
+ const watcher = watch(TELEMETRY_DIRECTORY, { recursive: true }, () => {
184
183
  process.stdout.write("\x1b[2J\x1b[H");
185
184
  printEvents();
186
185
  });
@@ -195,16 +194,21 @@ if (command === "telemetry") {
195
194
  process.exit(0);
196
195
  }
197
196
  if (action === "reset") {
198
- delete config.telemetry;
199
- await saveConfig(config);
200
- try {
201
- unlinkSync(telemetryFile);
197
+ if (values.all && telemetryProfile)
198
+ fail("Use either a profile name or --all, not both.");
199
+ if (!values.all && !telemetryProfile)
200
+ fail("Usage: agent-dir telemetry reset <profile> | --all");
201
+ if (values.all) {
202
+ delete config.telemetry;
203
+ await saveConfig(config);
204
+ resetTelemetry(undefined, true);
205
+ console.log("✓ Telemetry settings reset and all recorded data was cleared.");
202
206
  }
203
- catch (error) {
204
- if (isRecord(error) && error.code !== "ENOENT")
205
- throw error;
207
+ else {
208
+ migrateLegacyTelemetry();
209
+ resetTelemetry(telemetryProfile);
210
+ console.log(`✓ Telemetry data cleared for profile: ${telemetryProfile}`);
206
211
  }
207
- console.log("✓ Telemetry reset to defaults and all recorded data was cleared.");
208
212
  process.exit(0);
209
213
  }
210
214
  fail("Usage: agent-dir telemetry {status|enable|disable|schema|show|summary|reset}");
@@ -223,6 +227,8 @@ if (command === "config") {
223
227
  const profile = structuredClone(config.profiles[name]);
224
228
  if (profile.token)
225
229
  profile.token = "<configured>";
230
+ if (profile.tunnels?.cloudflared?.token)
231
+ profile.tunnels.cloudflared.token = "<configured>";
226
232
  console.log(JSON.stringify(profile, null, 2));
227
233
  process.exit(0);
228
234
  }
@@ -283,8 +289,21 @@ if (command === "config") {
283
289
  fail("Usage: agent-dir config add <name> [options]");
284
290
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name))
285
291
  fail("Profile names may contain letters, numbers, dots, underscores, and hyphens.");
286
- const tunnel = values.tunnel ?? "none";
287
- if (tunnel !== "none" && tunnel !== "wormhole")
292
+ if (values.wormhole && values.cloudflared)
293
+ fail("Choose only one of -w or -c.");
294
+ if ((values.wormhole || values.cloudflared) && values.tunnel)
295
+ fail("Use either -w/-c or --tunnel, not both.");
296
+ const tunnel = values.wormhole
297
+ ? "wormhole"
298
+ : values.cloudflared
299
+ ? "cloudflared"
300
+ : (values.tunnel ??
301
+ (values.hostname
302
+ ? "cloudflared"
303
+ : values.subdomain || values.random
304
+ ? "wormhole"
305
+ : "none"));
306
+ if (tunnel !== "none" && tunnel !== "wormhole" && tunnel !== "cloudflared")
288
307
  fail(`Unsupported tunnel provider: ${tunnel}`);
289
308
  const profile = {
290
309
  directory: resolve(values.directory ?? "."),
@@ -294,6 +313,28 @@ if (command === "config") {
294
313
  };
295
314
  if (values.subdomain)
296
315
  profile.subdomain = values.subdomain;
316
+ if (values.hostname)
317
+ profile.hostname = values.hostname;
318
+ if (values.subdomain || values.hostname || values.random) {
319
+ profile.tunnels = {
320
+ ...(values.subdomain || values.random
321
+ ? {
322
+ wormhole: {
323
+ ...(values.subdomain ? { subdomain: values.subdomain } : { random: true }),
324
+ },
325
+ }
326
+ : {}),
327
+ ...(values.hostname
328
+ ? {
329
+ cloudflared: {
330
+ hostname: values.hostname,
331
+ ...(values["cloudflared-token"] ? { token: values["cloudflared-token"] } : {}),
332
+ },
333
+ }
334
+ : {}),
335
+ };
336
+ profile.defaultTunnel = tunnel;
337
+ }
297
338
  const npm = parseList(values.npm);
298
339
  if (npm !== undefined)
299
340
  profile.npm = { allowedScripts: npm };
@@ -308,6 +349,9 @@ if (command === "config") {
308
349
  if (profile.tunnel === "wormhole" && !profile.subdomain && !values.random) {
309
350
  fail("Wormhole profiles require --subdomain <name>, or use --random.");
310
351
  }
352
+ if (profile.tunnel === "cloudflared" && !profile.hostname) {
353
+ fail("Cloudflared profiles require --hostname <host> (a hostname already routed to your named Cloudflare Tunnel).");
354
+ }
311
355
  config.profiles[name] = profile;
312
356
  await saveConfig(config);
313
357
  console.log(`✓ Saved profile '${name}'.`);
@@ -323,17 +367,37 @@ if (command && command !== "config" && !profile && command.startsWith("-")) {
323
367
  const positionalDirectory = profile ? positionals[1] : positionals[0];
324
368
  const directory = resolve(values.directory ?? profile?.directory ?? positionalDirectory ?? ".");
325
369
  const port = parsePort(values.port, profile?.port ?? 3002);
326
- const tunnelValue = values["no-tunnel"] ? "none" : (values.tunnel ?? profile?.tunnel ?? "none");
327
- if (tunnelValue !== "none" && tunnelValue !== "wormhole") {
370
+ if (values.wormhole && values.cloudflared)
371
+ fail("Choose only one of -w or -c.");
372
+ if ((values.wormhole || values.cloudflared) && values.tunnel)
373
+ fail("Use either -w/-c or --tunnel, not both.");
374
+ const configuredDefault = profile?.defaultTunnel ??
375
+ (profile?.tunnels?.cloudflared ? "cloudflared" : (profile?.tunnel ?? "none"));
376
+ const tunnelValue = values["no-tunnel"]
377
+ ? "none"
378
+ : values.wormhole
379
+ ? "wormhole"
380
+ : values.cloudflared
381
+ ? "cloudflared"
382
+ : (values.tunnel ?? configuredDefault);
383
+ if (tunnelValue !== "none" && tunnelValue !== "wormhole" && tunnelValue !== "cloudflared") {
328
384
  fail(`Unsupported tunnel provider: ${tunnelValue}`);
329
385
  }
330
386
  const tunnel = tunnelValue;
331
- const randomTunnel = values.random === true;
332
- const subdomain = randomTunnel ? undefined : (values.subdomain ?? profile?.subdomain);
387
+ const randomTunnel = values.random === true ||
388
+ (values.subdomain === undefined && profile?.tunnels?.wormhole?.random === true);
389
+ const subdomain = randomTunnel
390
+ ? undefined
391
+ : (values.subdomain ?? profile?.tunnels?.wormhole?.subdomain ?? profile?.subdomain);
392
+ const hostname = values.hostname ?? profile?.tunnels?.cloudflared?.hostname ?? profile?.hostname;
393
+ const cloudflaredToken = values["cloudflared-token"] ?? profile?.tunnels?.cloudflared?.token;
333
394
  if (tunnel === "wormhole" && !subdomain && !randomTunnel) {
334
395
  fail("Wormhole requires --subdomain <name>, or use --random.");
335
396
  }
336
- if (randomTunnel && tunnel === "none") {
397
+ if (tunnel === "cloudflared" && !hostname) {
398
+ fail("Cloudflared requires --hostname <host> (a hostname already routed to your named Cloudflare Tunnel).");
399
+ }
400
+ if (randomTunnel && tunnel !== "wormhole") {
337
401
  fail("--random requires --tunnel wormhole.");
338
402
  }
339
403
  const token = values.token ?? profile?.token ?? randomBytes(24).toString("hex");
@@ -358,6 +422,7 @@ const telemetryConfigId = profile
358
422
  port,
359
423
  tunnel,
360
424
  subdomain,
425
+ hostname,
361
426
  randomTunnel,
362
427
  npm: commandConfig.npm?.allowedScripts ?? [],
363
428
  commands: commandConfig.commands ?? [],
@@ -377,6 +442,8 @@ if (profile && command)
377
442
  bannerOptions.profileName = command;
378
443
  if (subdomain)
379
444
  bannerOptions.subdomain = subdomain;
445
+ if (hostname)
446
+ bannerOptions.hostname = hostname;
380
447
  if (npmScripts)
381
448
  bannerOptions.npmScripts = npmScripts;
382
449
  if (allowedCommands)
@@ -386,6 +453,7 @@ if (commandConfig.blacklistedCommands?.length) {
386
453
  }
387
454
  bannerOptions.gitEnabled = commandConfig.git ?? commandConfig.commands?.includes("git") ?? false;
388
455
  printBanner(bannerOptions);
456
+ const telemetrySessionId = randomUUID();
389
457
  const server = await startServer({
390
458
  root: directory,
391
459
  port,
@@ -394,6 +462,7 @@ const server = await startServer({
394
462
  telemetry: {
395
463
  ...(config.telemetry ?? { level: "none" }),
396
464
  ...(telemetryConfigId ? { configId: telemetryConfigId } : {}),
465
+ sessionId: telemetrySessionId,
397
466
  persist: true,
398
467
  },
399
468
  });
@@ -405,12 +474,15 @@ console.log(" Token : <configured>");
405
474
  if (tunnel !== "none") {
406
475
  try {
407
476
  tunnelResult = await startTunnel({
408
- provider: "wormhole",
477
+ provider: tunnel,
409
478
  port,
479
+ ...(hostname ? { hostname } : {}),
480
+ ...(cloudflaredToken ? { cloudflaredToken } : {}),
410
481
  token,
411
482
  telemetry: {
412
483
  ...(config.telemetry ?? { level: "none" }),
413
484
  ...(telemetryConfigId ? { configId: telemetryConfigId } : {}),
485
+ sessionId: telemetrySessionId,
414
486
  persist: true,
415
487
  },
416
488
  random: randomTunnel,
@@ -453,14 +525,3 @@ async function confirmConfigDelete(message, skipPrompt) {
453
525
  function isRecord(value) {
454
526
  return typeof value === "object" && value !== null && !Array.isArray(value);
455
527
  }
456
- function readTelemetryEvents(path) {
457
- try {
458
- return readFileSync(path, "utf8")
459
- .split("\n")
460
- .filter(Boolean)
461
- .map((line) => JSON.parse(line));
462
- }
463
- catch {
464
- return [];
465
- }
466
- }
@@ -63,12 +63,42 @@ function isProfile(value) {
63
63
  return false;
64
64
  if (port < 1 || port > 65_535)
65
65
  return false;
66
- if (profile.tunnel !== "none" && profile.tunnel !== "wormhole")
66
+ if (profile.tunnel !== "none" &&
67
+ profile.tunnel !== "wormhole" &&
68
+ profile.tunnel !== "cloudflared")
67
69
  return false;
68
70
  if (profile.subdomain !== undefined && typeof profile.subdomain !== "string")
69
71
  return false;
72
+ if (profile.hostname !== undefined && typeof profile.hostname !== "string")
73
+ return false;
70
74
  if (profile.token !== undefined && typeof profile.token !== "string")
71
75
  return false;
76
+ if (profile.defaultTunnel !== undefined &&
77
+ !["none", "wormhole", "cloudflared"].includes(String(profile.defaultTunnel)))
78
+ return false;
79
+ if (profile.tunnels !== undefined) {
80
+ if (!profile.tunnels || typeof profile.tunnels !== "object")
81
+ return false;
82
+ const tunnels = profile.tunnels;
83
+ if (tunnels.wormhole !== undefined) {
84
+ if (!tunnels.wormhole || typeof tunnels.wormhole !== "object")
85
+ return false;
86
+ const wormhole = tunnels.wormhole;
87
+ if (wormhole.subdomain !== undefined && typeof wormhole.subdomain !== "string")
88
+ return false;
89
+ if (wormhole.random !== undefined && typeof wormhole.random !== "boolean")
90
+ return false;
91
+ }
92
+ if (tunnels.cloudflared !== undefined) {
93
+ if (!tunnels.cloudflared || typeof tunnels.cloudflared !== "object")
94
+ return false;
95
+ const cloudflared = tunnels.cloudflared;
96
+ if (typeof cloudflared.hostname !== "string" || !cloudflared.hostname)
97
+ return false;
98
+ if (cloudflared.token !== undefined && typeof cloudflared.token !== "string")
99
+ return false;
100
+ }
101
+ }
72
102
  if (profile.npm !== undefined) {
73
103
  if (!profile.npm || typeof profile.npm !== "object")
74
104
  return false;
@@ -6,6 +6,7 @@ export declare function printBanner(options: {
6
6
  tunnel: string;
7
7
  profileName?: string;
8
8
  subdomain?: string;
9
+ hostname?: string;
9
10
  token?: string;
10
11
  npmScripts?: string[];
11
12
  commands?: string[];
@@ -116,8 +116,12 @@ export function printHelp() {
116
116
  writeLine("");
117
117
  writeLine("Options:");
118
118
  writeLine(" -p, --port <port> Local server port");
119
- writeLine(" -t, --tunnel <provider> Tunnel provider (none, wormhole)");
119
+ writeLine(" -t, --tunnel <provider> Explicit tunnel provider (none, wormhole, cloudflared)");
120
+ writeLine(" -w, --wormhole Use Wormhole for this run");
121
+ writeLine(" -c, --cloudflared Use Cloudflare Tunnel for this run");
120
122
  writeLine(" -s, --subdomain <name> Wormhole subdomain");
123
+ writeLine(" --hostname <host> Hostname routed to a Cloudflare Tunnel");
124
+ writeLine(" --cloudflared-token <token> Cloudflare Tunnel connector token (stored in config)");
121
125
  writeLine(" --directory <path> Directory to expose");
122
126
  writeLine(" --npm <scripts> Comma-separated allowed npm scripts");
123
127
  writeLine(" --command <commands> Comma-separated allowed commands");
@@ -169,6 +173,8 @@ export function printBanner(options) {
169
173
  }
170
174
  if (options.subdomain)
171
175
  writeLine(` Domain https://${options.subdomain}.wormhole.bar`);
176
+ if (options.hostname)
177
+ writeLine(` Domain https://${options.hostname}`);
172
178
  writeLine("");
173
179
  writeLine(" MCP");
174
180
  writeLine(` ${color("⚡ /mcp", CYAN)}`);