agent-dir 0.4.1 β†’ 1.0.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.
@@ -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,12 +42,15 @@ 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" },
48
53
  token: { type: "string" },
49
- git: { type: "boolean" },
50
54
  rotate: { type: "boolean" },
51
55
  "no-tunnel": { type: "boolean" },
52
56
  random: { type: "boolean" },
@@ -75,7 +79,8 @@ if (command === "setup") {
75
79
  if (shouldOfferSetup(await hasConfigFile(), {
76
80
  random: values.random,
77
81
  noTunnel: values["no-tunnel"],
78
- tunnel: values.tunnel,
82
+ tunnel: values.tunnel ??
83
+ (values.wormhole ? "wormhole" : values.cloudflared ? "cloudflared" : undefined),
79
84
  }) &&
80
85
  command !== "config" &&
81
86
  command !== "telemetry") {
@@ -90,7 +95,8 @@ if (shouldOfferSetup(await hasConfigFile(), {
90
95
  if (command === "telemetry") {
91
96
  const action = positionals[1];
92
97
  const currentLevel = config.telemetry?.level ?? "none";
93
- const telemetryFile = resolve(`${process.env.HOME ?? "."}/.config/agent-dir/telemetry.jsonl`);
98
+ const telemetryProfile = positionals[2];
99
+ const telemetrySession = positionals[3];
94
100
  if (action === "status") {
95
101
  console.log(`Telemetry: ${currentLevel === "none" ? "disabled" : "enabled"}`);
96
102
  console.log(`Level: ${currentLevel}`);
@@ -138,12 +144,13 @@ if (command === "telemetry") {
138
144
  process.exit(0);
139
145
  }
140
146
  if (action === "summary") {
141
- console.log(formatTelemetrySummary(summarizeTelemetry(readTelemetryEvents(telemetryFile)), currentLevel));
147
+ console.log(formatTelemetrySummary(summarizeTelemetry(readAllTelemetryEvents().filter((event) => !telemetryProfile || event.configId === telemetryProfile)), currentLevel));
142
148
  process.exit(0);
143
149
  }
144
150
  if (action === "show") {
145
151
  const printEvents = () => {
146
- const events = readTelemetryEvents(telemetryFile);
152
+ const events = readAllTelemetryEvents().filter((event) => (!telemetryProfile || event.configId === telemetryProfile) &&
153
+ (!telemetrySession || event.sessionId === telemetrySession));
147
154
  const grouped = new Map();
148
155
  for (const event of events) {
149
156
  const configId = isRecord(event) && typeof event.configId === "string" ? event.configId : "unknown";
@@ -170,17 +177,8 @@ if (command === "telemetry") {
170
177
  };
171
178
  printEvents();
172
179
  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, () => {
180
+ ensureTelemetryDirectory();
181
+ const watcher = watch(TELEMETRY_DIRECTORY, { recursive: true }, () => {
184
182
  process.stdout.write("\x1b[2J\x1b[H");
185
183
  printEvents();
186
184
  });
@@ -195,16 +193,21 @@ if (command === "telemetry") {
195
193
  process.exit(0);
196
194
  }
197
195
  if (action === "reset") {
198
- delete config.telemetry;
199
- await saveConfig(config);
200
- try {
201
- unlinkSync(telemetryFile);
196
+ if (values.all && telemetryProfile)
197
+ fail("Use either a profile name or --all, not both.");
198
+ if (!values.all && !telemetryProfile)
199
+ fail("Usage: agent-dir telemetry reset <profile> | --all");
200
+ if (values.all) {
201
+ delete config.telemetry;
202
+ await saveConfig(config);
203
+ resetTelemetry(undefined, true);
204
+ console.log("βœ“ Telemetry settings reset and all recorded data was cleared.");
202
205
  }
203
- catch (error) {
204
- if (isRecord(error) && error.code !== "ENOENT")
205
- throw error;
206
+ else {
207
+ migrateLegacyTelemetry();
208
+ resetTelemetry(telemetryProfile);
209
+ console.log(`βœ“ Telemetry data cleared for profile: ${telemetryProfile}`);
206
210
  }
207
- console.log("βœ“ Telemetry reset to defaults and all recorded data was cleared.");
208
211
  process.exit(0);
209
212
  }
210
213
  fail("Usage: agent-dir telemetry {status|enable|disable|schema|show|summary|reset}");
@@ -223,6 +226,8 @@ if (command === "config") {
223
226
  const profile = structuredClone(config.profiles[name]);
224
227
  if (profile.token)
225
228
  profile.token = "<configured>";
229
+ if (profile.tunnels?.cloudflared?.token)
230
+ profile.tunnels.cloudflared.token = "<configured>";
226
231
  console.log(JSON.stringify(profile, null, 2));
227
232
  process.exit(0);
228
233
  }
@@ -283,8 +288,21 @@ if (command === "config") {
283
288
  fail("Usage: agent-dir config add <name> [options]");
284
289
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name))
285
290
  fail("Profile names may contain letters, numbers, dots, underscores, and hyphens.");
286
- const tunnel = values.tunnel ?? "none";
287
- if (tunnel !== "none" && tunnel !== "wormhole")
291
+ if (values.wormhole && values.cloudflared)
292
+ fail("Choose only one of -w or -c.");
293
+ if ((values.wormhole || values.cloudflared) && values.tunnel)
294
+ fail("Use either -w/-c or --tunnel, not both.");
295
+ const tunnel = values.wormhole
296
+ ? "wormhole"
297
+ : values.cloudflared
298
+ ? "cloudflared"
299
+ : (values.tunnel ??
300
+ (values.hostname
301
+ ? "cloudflared"
302
+ : values.subdomain || values.random
303
+ ? "wormhole"
304
+ : "none"));
305
+ if (tunnel !== "none" && tunnel !== "wormhole" && tunnel !== "cloudflared")
288
306
  fail(`Unsupported tunnel provider: ${tunnel}`);
289
307
  const profile = {
290
308
  directory: resolve(values.directory ?? "."),
@@ -294,6 +312,28 @@ if (command === "config") {
294
312
  };
295
313
  if (values.subdomain)
296
314
  profile.subdomain = values.subdomain;
315
+ if (values.hostname)
316
+ profile.hostname = values.hostname;
317
+ if (values.subdomain || values.hostname || values.random) {
318
+ profile.tunnels = {
319
+ ...(values.subdomain || values.random
320
+ ? {
321
+ wormhole: {
322
+ ...(values.subdomain ? { subdomain: values.subdomain } : { random: true }),
323
+ },
324
+ }
325
+ : {}),
326
+ ...(values.hostname
327
+ ? {
328
+ cloudflared: {
329
+ hostname: values.hostname,
330
+ ...(values["cloudflared-token"] ? { token: values["cloudflared-token"] } : {}),
331
+ },
332
+ }
333
+ : {}),
334
+ };
335
+ profile.defaultTunnel = tunnel;
336
+ }
297
337
  const npm = parseList(values.npm);
298
338
  if (npm !== undefined)
299
339
  profile.npm = { allowedScripts: npm };
@@ -303,11 +343,12 @@ if (command === "config") {
303
343
  const blacklist = parseList(values.blacklist);
304
344
  if (blacklist !== undefined)
305
345
  profile.blacklistedCommands = blacklist;
306
- if (values.git !== undefined)
307
- profile.git = values.git;
308
346
  if (profile.tunnel === "wormhole" && !profile.subdomain && !values.random) {
309
347
  fail("Wormhole profiles require --subdomain <name>, or use --random.");
310
348
  }
349
+ if (profile.tunnel === "cloudflared" && !profile.hostname) {
350
+ fail("Cloudflared profiles require --hostname <host> (a hostname already routed to your named Cloudflare Tunnel).");
351
+ }
311
352
  config.profiles[name] = profile;
312
353
  await saveConfig(config);
313
354
  console.log(`βœ“ Saved profile '${name}'.`);
@@ -323,17 +364,37 @@ if (command && command !== "config" && !profile && command.startsWith("-")) {
323
364
  const positionalDirectory = profile ? positionals[1] : positionals[0];
324
365
  const directory = resolve(values.directory ?? profile?.directory ?? positionalDirectory ?? ".");
325
366
  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") {
367
+ if (values.wormhole && values.cloudflared)
368
+ fail("Choose only one of -w or -c.");
369
+ if ((values.wormhole || values.cloudflared) && values.tunnel)
370
+ fail("Use either -w/-c or --tunnel, not both.");
371
+ const configuredDefault = profile?.defaultTunnel ??
372
+ (profile?.tunnels?.cloudflared ? "cloudflared" : (profile?.tunnel ?? "none"));
373
+ const tunnelValue = values["no-tunnel"]
374
+ ? "none"
375
+ : values.wormhole
376
+ ? "wormhole"
377
+ : values.cloudflared
378
+ ? "cloudflared"
379
+ : (values.tunnel ?? configuredDefault);
380
+ if (tunnelValue !== "none" && tunnelValue !== "wormhole" && tunnelValue !== "cloudflared") {
328
381
  fail(`Unsupported tunnel provider: ${tunnelValue}`);
329
382
  }
330
383
  const tunnel = tunnelValue;
331
- const randomTunnel = values.random === true;
332
- const subdomain = randomTunnel ? undefined : (values.subdomain ?? profile?.subdomain);
384
+ const randomTunnel = values.random === true ||
385
+ (values.subdomain === undefined && profile?.tunnels?.wormhole?.random === true);
386
+ const subdomain = randomTunnel
387
+ ? undefined
388
+ : (values.subdomain ?? profile?.tunnels?.wormhole?.subdomain ?? profile?.subdomain);
389
+ const hostname = values.hostname ?? profile?.tunnels?.cloudflared?.hostname ?? profile?.hostname;
390
+ const cloudflaredToken = values["cloudflared-token"] ?? profile?.tunnels?.cloudflared?.token;
333
391
  if (tunnel === "wormhole" && !subdomain && !randomTunnel) {
334
392
  fail("Wormhole requires --subdomain <name>, or use --random.");
335
393
  }
336
- if (randomTunnel && tunnel === "none") {
394
+ if (tunnel === "cloudflared" && !hostname) {
395
+ fail("Cloudflared requires --hostname <host> (a hostname already routed to your named Cloudflare Tunnel).");
396
+ }
397
+ if (randomTunnel && tunnel !== "wormhole") {
337
398
  fail("--random requires --tunnel wormhole.");
338
399
  }
339
400
  const token = values.token ?? profile?.token ?? randomBytes(24).toString("hex");
@@ -347,8 +408,6 @@ if (commands !== undefined)
347
408
  overrides.commands = commands;
348
409
  if (blacklist !== undefined)
349
410
  overrides.blacklistedCommands = blacklist;
350
- if (values.git !== undefined)
351
- overrides.git = values.git;
352
411
  const commandConfig = mergeProfile(profile ?? { directory, port, tunnel }, overrides);
353
412
  const telemetryConfigId = profile
354
413
  ? command
@@ -358,11 +417,11 @@ const telemetryConfigId = profile
358
417
  port,
359
418
  tunnel,
360
419
  subdomain,
420
+ hostname,
361
421
  randomTunnel,
362
422
  npm: commandConfig.npm?.allowedScripts ?? [],
363
423
  commands: commandConfig.commands ?? [],
364
424
  blacklistedCommands: commandConfig.blacklistedCommands ?? [],
365
- git: commandConfig.git ?? commandConfig.commands?.includes("git") ?? false,
366
425
  }))
367
426
  .digest("hex")
368
427
  .slice(0, 12);
@@ -377,6 +436,8 @@ if (profile && command)
377
436
  bannerOptions.profileName = command;
378
437
  if (subdomain)
379
438
  bannerOptions.subdomain = subdomain;
439
+ if (hostname)
440
+ bannerOptions.hostname = hostname;
380
441
  if (npmScripts)
381
442
  bannerOptions.npmScripts = npmScripts;
382
443
  if (allowedCommands)
@@ -384,8 +445,8 @@ if (allowedCommands)
384
445
  if (commandConfig.blacklistedCommands?.length) {
385
446
  bannerOptions.blacklistedCommands = commandConfig.blacklistedCommands;
386
447
  }
387
- bannerOptions.gitEnabled = commandConfig.git ?? commandConfig.commands?.includes("git") ?? false;
388
448
  printBanner(bannerOptions);
449
+ const telemetrySessionId = randomUUID();
389
450
  const server = await startServer({
390
451
  root: directory,
391
452
  port,
@@ -394,6 +455,7 @@ const server = await startServer({
394
455
  telemetry: {
395
456
  ...(config.telemetry ?? { level: "none" }),
396
457
  ...(telemetryConfigId ? { configId: telemetryConfigId } : {}),
458
+ sessionId: telemetrySessionId,
397
459
  persist: true,
398
460
  },
399
461
  });
@@ -405,12 +467,15 @@ console.log(" Token : <configured>");
405
467
  if (tunnel !== "none") {
406
468
  try {
407
469
  tunnelResult = await startTunnel({
408
- provider: "wormhole",
470
+ provider: tunnel,
409
471
  port,
472
+ ...(hostname ? { hostname } : {}),
473
+ ...(cloudflaredToken ? { cloudflaredToken } : {}),
410
474
  token,
411
475
  telemetry: {
412
476
  ...(config.telemetry ?? { level: "none" }),
413
477
  ...(telemetryConfigId ? { configId: telemetryConfigId } : {}),
478
+ sessionId: telemetrySessionId,
414
479
  persist: true,
415
480
  },
416
481
  random: randomTunnel,
@@ -453,14 +518,3 @@ async function confirmConfigDelete(message, skipPrompt) {
453
518
  function isRecord(value) {
454
519
  return typeof value === "object" && value !== null && !Array.isArray(value);
455
520
  }
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/auth.js CHANGED
@@ -14,7 +14,9 @@ function secureCompare(left, right) {
14
14
  return timingSafeEqual(leftHash, rightHash);
15
15
  }
16
16
  export function unauthorized() {
17
- return new Response(JSON.stringify({ error: "Unauthorized" }), {
17
+ return new Response(JSON.stringify({
18
+ error: "Unauthorized: provide a valid Authorization: Bearer <token> header.",
19
+ }), {
18
20
  status: 401,
19
21
  headers: {
20
22
  "content-type": "application/json; charset=utf-8",
@@ -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;
@@ -87,7 +117,5 @@ function isProfile(value) {
87
117
  profile.blacklistedCommands.some((item) => typeof item !== "string"))
88
118
  return false;
89
119
  }
90
- if (profile.git !== undefined && typeof profile.git !== "boolean")
91
- return false;
92
120
  return true;
93
121
  }
@@ -6,11 +6,11 @@ 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[];
12
13
  blacklistedCommands?: string[];
13
- gitEnabled?: boolean;
14
14
  }): void;
15
15
  export declare function printRequestLog(entry: RequestLog): void;
16
16
  export declare function logRequest(entry: RequestLog): void;
@@ -9,53 +9,24 @@ const BRIGHT_BLUE = "\x1b[94m";
9
9
  const BRIGHT_CYAN = "\x1b[96m";
10
10
  const BRIGHT_MAGENTA = "\x1b[95m";
11
11
  const BRIGHT_YELLOW = "\x1b[93m";
12
- const BRIGHT_RED = "\x1b[91m";
13
- const BRIGHT_GREEN = "\x1b[92m";
14
12
  const writeLine = (line = "") => {
15
13
  process.stdout.write(`${line}\r\n`);
16
14
  };
17
15
  const color = (value, code) => `${code}${value}${RESET}`;
18
16
  const TOOL_ICONS = {
19
- list_files: "πŸ“‚",
20
- list_dirs: "πŸ—‚οΈ",
21
17
  read_range: "πŸ“",
22
- read_files: "πŸ“š",
23
- write_files: "πŸ“",
24
- patch_files: "🧩",
25
- delete_files: "πŸ—‘οΈ",
26
18
  run_npm_batch: "πŸ”§",
27
19
  run_command_batch: "πŸ› οΈ",
28
- search_files: "πŸ”Ž",
29
20
  find_files: "🧭",
30
21
  search_code: "🧠",
31
22
  find_symbol: "πŸ”Ή",
32
- find_definition: "🎯",
33
23
  find_references: "πŸ”—",
34
- find_imports: "β†ͺ",
35
- find_exports: "β†—",
36
- git_status: "🌿",
37
- git_stage: "βž•",
38
- git_unstage: "βž–",
39
- git_commit: "●",
40
- git_restore: "↩",
41
- git_push: "⬆",
42
- git_diff: "πŸ“",
43
- git_log: "πŸ“œ",
44
- project_overview: "πŸ—ΊοΈ",
45
- package_info: "πŸ“¦",
46
- allowed_commands: "πŸ”",
47
24
  file_info: "ℹ️",
48
- diagnostics: "🩺",
49
25
  project_context: "🧭",
50
- inspect: "πŸ”Ž",
51
- locate: "🎯",
52
- read_relevant: "🎯",
53
- git_changes: "πŸ“‹",
54
26
  apply_changes: "✏️",
55
27
  validate: "βœ“",
56
28
  };
57
29
  const MCP_METHOD_ICONS = {
58
- initialize: "πŸš€",
59
30
  "notifications/initialized": "βœ“",
60
31
  "notifications/cancelled": "βœ–",
61
32
  "notifications/progress": "⏳",
@@ -80,19 +51,8 @@ const MCP_METHOD_ICONS = {
80
51
  const TOOL_NAME_WIDTH = Math.max(...Object.keys(TOOL_ICONS).map((name) => name.length));
81
52
  const MCP_METHOD_NAME_WIDTH = Math.max(...Object.keys(MCP_METHOD_ICONS).map((name) => name.length));
82
53
  const TOOL_COLORS = {
83
- list_files: CYAN,
84
- list_dirs: BRIGHT_CYAN,
85
- read_files: BRIGHT_GREEN,
86
- write_files: BRIGHT_YELLOW,
87
- patch_files: BRIGHT_MAGENTA,
88
- delete_files: BRIGHT_RED,
89
54
  run_npm_batch: BRIGHT_BLUE,
90
55
  run_command_batch: BRIGHT_CYAN,
91
- git_stage: YELLOW,
92
- git_unstage: MAGENTA,
93
- git_commit: BRIGHT_GREEN,
94
- git_restore: RED,
95
- git_push: BRIGHT_BLUE,
96
56
  };
97
57
  const NARROW_TOOL_ICONS = new Set(["✎", "βœ–", "βš™", "⌘", "β†ͺ", "β†—"]);
98
58
  const TOOL_ICON_SLOT_WIDTH = 3;
@@ -116,14 +76,16 @@ export function printHelp() {
116
76
  writeLine("");
117
77
  writeLine("Options:");
118
78
  writeLine(" -p, --port <port> Local server port");
119
- writeLine(" -t, --tunnel <provider> Tunnel provider (none, wormhole)");
79
+ writeLine(" -t, --tunnel <provider> Explicit tunnel provider (none, wormhole, cloudflared)");
80
+ writeLine(" -w, --wormhole Use Wormhole for this run");
81
+ writeLine(" -c, --cloudflared Use Cloudflare Tunnel for this run");
120
82
  writeLine(" -s, --subdomain <name> Wormhole subdomain");
83
+ writeLine(" --hostname <host> Hostname routed to a Cloudflare Tunnel");
84
+ writeLine(" --cloudflared-token <token> Cloudflare Tunnel connector token (stored in config)");
121
85
  writeLine(" --directory <path> Directory to expose");
122
86
  writeLine(" --npm <scripts> Comma-separated allowed npm scripts");
123
87
  writeLine(" --command <commands> Comma-separated allowed commands");
124
88
  writeLine(" --blacklist <commands> Comma-separated blocked command prefixes");
125
- writeLine(" --git Enable dedicated Git MCP tools");
126
- writeLine(" --no-git Disable dedicated Git MCP tools");
127
89
  writeLine(" --token <token> Explicit Bearer token");
128
90
  writeLine(" --no-tunnel Disable tunneling");
129
91
  writeLine(" --random Use a random Wormhole URL (no subdomain)");
@@ -169,43 +131,24 @@ export function printBanner(options) {
169
131
  }
170
132
  if (options.subdomain)
171
133
  writeLine(` Domain https://${options.subdomain}.wormhole.bar`);
134
+ if (options.hostname)
135
+ writeLine(` Domain https://${options.hostname}`);
172
136
  writeLine("");
173
137
  writeLine(" MCP");
174
138
  writeLine(` ${color("⚑ /mcp", CYAN)}`);
175
139
  writeLine("");
176
140
  writeLine(" TOOLS");
177
- writeLine(` ${color(toolLabel("list_files"), CYAN)}`);
178
- writeLine(` ${color(toolLabel("list_dirs"), BRIGHT_CYAN)}`);
179
- writeLine(` ${color(toolLabel("read_range"), GREEN)}`);
180
- writeLine(` ${color(toolLabel("read_files"), BRIGHT_GREEN)}`);
181
- writeLine(` ${color(toolLabel("write_files"), BRIGHT_YELLOW)}`);
182
- writeLine(` ${color(toolLabel("patch_files"), BRIGHT_MAGENTA)}`);
183
- writeLine(` ${color(toolLabel("delete_files"), BRIGHT_RED)}`);
184
- writeLine(` ${color(toolLabel("run_npm_batch"), BRIGHT_BLUE)}`);
185
- writeLine(` ${color(toolLabel("run_command_batch"), BRIGHT_CYAN)}`);
186
- writeLine(` ${color(toolLabel("search_files"), CYAN)}`);
141
+ writeLine(` ${color(toolLabel("project_context"), BRIGHT_CYAN)}`);
187
142
  writeLine(` ${color(toolLabel("find_files"), CYAN)}`);
188
143
  writeLine(` ${color(toolLabel("search_code"), BRIGHT_CYAN)}`);
189
144
  writeLine(` ${color(toolLabel("find_symbol"), GREEN)}`);
190
- writeLine(` ${color(toolLabel("find_definition"), GREEN)}`);
191
145
  writeLine(` ${color(toolLabel("find_references"), GREEN)}`);
192
- writeLine(` ${color(toolLabel("find_imports"), BRIGHT_GREEN)}`);
193
- writeLine(` ${color(toolLabel("find_exports"), BRIGHT_GREEN)}`);
194
- if (options.gitEnabled) {
195
- writeLine(` ${color(toolLabel("git_status"), CYAN)}`);
196
- writeLine(` ${color(toolLabel("git_stage"), YELLOW)}`);
197
- writeLine(` ${color(toolLabel("git_unstage"), MAGENTA)}`);
198
- writeLine(` ${color(toolLabel("git_commit"), BRIGHT_GREEN)}`);
199
- writeLine(` ${color(toolLabel("git_restore"), RED)}`);
200
- writeLine(` ${color(toolLabel("git_push"), BRIGHT_BLUE)}`);
201
- writeLine(` ${color(toolLabel("git_diff"), CYAN)}`);
202
- writeLine(` ${color(toolLabel("git_log"), CYAN)}`);
203
- }
204
- writeLine(` ${color(toolLabel("project_overview"), BRIGHT_CYAN)}`);
205
- writeLine(` ${color(toolLabel("package_info"), YELLOW)}`);
206
- writeLine(` ${color(toolLabel("allowed_commands"), YELLOW)}`);
146
+ writeLine(` ${color(toolLabel("read_range"), GREEN)}`);
147
+ writeLine(` ${color(toolLabel("apply_changes"), BRIGHT_MAGENTA)}`);
148
+ writeLine(` ${color(toolLabel("validate"), BRIGHT_YELLOW)}`);
149
+ writeLine(` ${color(toolLabel("run_npm_batch"), BRIGHT_BLUE)}`);
150
+ writeLine(` ${color(toolLabel("run_command_batch"), BRIGHT_CYAN)}`);
207
151
  writeLine(` ${color(toolLabel("file_info"), YELLOW)}`);
208
- writeLine(` ${color(toolLabel("diagnostics"), BRIGHT_YELLOW)}`);
209
152
  writeLine("");
210
153
  if (options.npmScripts?.length) {
211
154
  writeLine(" NPM COMMANDS");