agent-dir 0.4.1 → 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 +59 -12
- package/dist/bin/agent-dir.js +105 -44
- package/dist/src/config.js +31 -1
- package/dist/src/logging.d.ts +1 -0
- package/dist/src/logging.js +7 -1
- package/dist/src/mcp.js +115 -14
- package/dist/src/server.js +6 -5
- package/dist/src/setup.js +67 -6
- package/dist/src/telemetry/privacy.js +46 -0
- package/dist/src/telemetry/recorder.js +8 -17
- package/dist/src/telemetry/storage.d.ts +9 -0
- package/dist/src/telemetry/storage.js +133 -0
- package/dist/src/telemetry/types.d.ts +22 -0
- package/dist/src/tools/code.js +1 -1
- package/dist/src/tools/commands.d.ts +7 -0
- package/dist/src/tools/commands.js +63 -5
- package/dist/src/tools/git.js +10 -4
- package/dist/src/tools/npm.js +7 -5
- package/dist/src/tunnel.js +35 -18
- package/dist/src/types.d.ts +16 -2
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/dist/bin/agent-dir.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import {
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
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
|
-
}
|
package/dist/src/config.js
CHANGED
|
@@ -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" &&
|
|
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;
|
package/dist/src/logging.d.ts
CHANGED
package/dist/src/logging.js
CHANGED
|
@@ -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>
|
|
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)}`);
|