auvrynt 1.0.1 → 1.0.2

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
@@ -124,10 +124,15 @@ Keep it private.
124
124
  | Command | Description |
125
125
  |---|---|
126
126
  | `auvrynt init` | Run first-time setup or update your config |
127
- | `auvrynt start` | Start the server with clean animated output |
128
- | `auvrynt serve` | Start the server with verbose log output |
129
- | `auvrynt doctor` | Show your config, Node version, and dependency health |
130
- | `auvrynt config get` | Print your saved configuration |
127
+ | `auvrynt start` | Start a Cloudflare tunnel scoped to the current directory |
128
+ | `auvrynt serve` | Start the server with verbose log output |
129
+ | `auvrynt doctor` | Show your config, Node version, and dependency health |
130
+ | `auvrynt status` | Show local MCP, Blender, Godot, Cloudflare Tunnel, and Serena status |
131
+ | `auvrynt connected` | Show recently observed ChatGPT, Kimi, Claude, or other MCP providers |
132
+ | `auvrynt uninstall` | Remove Auvrynt configuration after confirmation |
133
+ | `auvrynt uninstall -y` | Remove Auvrynt configuration without confirmation |
134
+ | `auvrynt help` | Print the complete command reference |
135
+ | `auvrynt config get` | Print your saved configuration |
131
136
  | `auvrynt config set publicBaseUrl <url>` | Update your public tunnel URL |
132
137
 
133
138
  **Override the tunnel URL for a one-off run:**
package/dist/cli.js CHANGED
@@ -1,13 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
- import { resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { existsSync, rmSync } from "node:fs";
6
+ import { mkdir, open, readFile, unlink, writeFile } from "node:fs/promises";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { execFileSync, spawn } from "node:child_process";
5
9
  import * as prompts from "@clack/prompts";
6
10
  import { getShellConfig } from "@earendil-works/pi-coding-agent";
7
11
  import { satisfies } from "semver";
8
12
  import { loadConfig } from "./config.js";
9
13
  import { generateOwnerToken, loadAuvryntFiles, writeAuvryntAuth, writeAuvryntConfig, } from "./user-config.js";
10
14
  import { expandHomePath } from "./roots.js";
15
+ import { discoverLocalIntegrations, processDetected } from "./integration-discovery.js";
16
+ import { readConnectedClients } from "./connection-registry.js";
11
17
  const require = createRequire(import.meta.url);
12
18
  const SUPPORTED_NODE_RANGE = ">=20.12 <27";
13
19
  async function main(argv) {
@@ -16,11 +22,42 @@ async function main(argv) {
16
22
  const command = normalizeCommand(rawCommand);
17
23
  switch (command) {
18
24
  case "serve":
19
- await ensureConfigured();
20
25
  if (rawCommand === "start") {
21
- process.env.AUVRYNT_START_MODE = "true";
26
+ // `start` is intentionally directory-scoped: the launch directory is
27
+ // the only project root available to the web agent for this session.
28
+ const launchRoot = resolve(process.cwd());
29
+ process.env.AUVRYNT_ALLOWED_ROOTS = launchRoot;
30
+ process.env.AUVRYNT_WORKTREE_ROOT = launchRoot;
31
+ }
32
+ await ensureConfigured({ directoryScoped: rawCommand === "start" });
33
+ const localConfig = loadConfig();
34
+ const instanceLock = await acquireInstanceLock(localConfig.stateDir);
35
+ let tunnel;
36
+ let stopTunnel;
37
+ try {
38
+ if (rawCommand === "start") {
39
+ process.env.AUVRYNT_START_MODE = "true";
40
+ process.env.AUVRYNT_SERENA_ENABLED = "true";
41
+ process.env.AUVRYNT_SERENA_EXECUTABLE = await ensureSerenaExecutable();
42
+ tunnel = await startCloudflareTunnel(localConfig.port);
43
+ process.env.AUVRYNT_PUBLIC_BASE_URL = tunnel.url;
44
+ stopTunnel = () => {
45
+ if (tunnel && !tunnel.process.killed)
46
+ tunnel.process.kill();
47
+ };
48
+ process.once("SIGINT", stopTunnel);
49
+ process.once("SIGTERM", stopTunnel);
50
+ }
51
+ await serve();
52
+ }
53
+ finally {
54
+ if (stopTunnel) {
55
+ process.removeListener("SIGINT", stopTunnel);
56
+ process.removeListener("SIGTERM", stopTunnel);
57
+ stopTunnel();
58
+ }
59
+ await instanceLock.release();
22
60
  }
23
- await serve();
24
61
  return;
25
62
  case "init":
26
63
  await runInit({ force: args.includes("--force") });
@@ -28,9 +65,21 @@ async function main(argv) {
28
65
  case "doctor":
29
66
  await runDoctor();
30
67
  return;
68
+ case "status":
69
+ await runStatus();
70
+ return;
71
+ case "connected":
72
+ runConnected();
73
+ return;
74
+ case "uninstall":
75
+ await runUninstall(args.includes("--yes") || args.includes("-y"));
76
+ return;
31
77
  case "config":
32
78
  runConfigCommand(args);
33
79
  return;
80
+ case "setup":
81
+ await runSetup();
82
+ return;
34
83
  case "help":
35
84
  printHelp();
36
85
  return;
@@ -41,16 +90,35 @@ function normalizeCommand(command) {
41
90
  return "serve";
42
91
  if (command === "init" || command === "doctor" || command === "config")
43
92
  return command;
93
+ if (command === "status" || command === "connected" || command === "uninstall")
94
+ return command;
95
+ if (command === "setup")
96
+ return "setup";
44
97
  if (command === "help" || command === "--help" || command === "-h")
45
98
  return "help";
46
99
  throw new Error(`Unknown command: ${command}`);
47
100
  }
48
- async function ensureConfigured() {
101
+ async function ensureConfigured(options = {}) {
49
102
  const files = loadAuvryntFiles();
50
103
  if (files.configExists && files.authExists)
51
104
  return;
52
105
  if (process.env.AUVRYNT_OAUTH_OWNER_TOKEN)
53
106
  return;
107
+ if (options.directoryScoped) {
108
+ const launchRoot = resolve(process.cwd());
109
+ if (!files.configExists) {
110
+ writeAuvryntConfig({
111
+ host: files.config.host ?? "127.0.0.1",
112
+ port: files.config.port ?? 49321,
113
+ allowedRoots: [launchRoot],
114
+ publicBaseUrl: files.config.publicBaseUrl ?? `http://127.0.0.1:${files.config.port ?? 49321}`,
115
+ });
116
+ }
117
+ if (!files.authExists) {
118
+ writeAuvryntAuth({ ownerToken: generateOwnerToken() });
119
+ }
120
+ return;
121
+ }
54
122
  if (!input.isTTY || !output.isTTY) {
55
123
  throw new Error([
56
124
  "Auvrynt is not configured and this terminal is non-interactive.",
@@ -129,7 +197,7 @@ async function runInit({ force }) {
129
197
  "Use this when ChatGPT or Claude asks you to approve Auvrynt access.",
130
198
  `Stored at: ${authPath}`,
131
199
  ].join("\n"), "Owner password");
132
- prompts.outro("Run `auvrynt serve` to start the MCP server.");
200
+ prompts.outro("Run `auvrynt start` to start the MCP server.");
133
201
  }
134
202
  catch (error) {
135
203
  if (error instanceof SetupCancelledError) {
@@ -159,51 +227,75 @@ async function serve() {
159
227
  const publicMcpUrl = config.publicBaseUrl
160
228
  ? `${config.publicBaseUrl.replace(/\/$/, "")}/mcp`
161
229
  : `http://${config.host}:${config.port}/mcp`;
162
- const httpServer = app.listen(config.port, config.host, () => {
163
- if (startMode) {
164
- console.clear();
165
- console.log("Auvrynt is running!");
166
- console.log("");
167
- console.log(` Public URL: \x1b[36m${publicMcpUrl}\x1b[0m`);
168
- console.log(` Owner Password: \x1b[33m${ownerToken}\x1b[0m`);
169
- console.log("");
170
- console.log(" # CTRL + C to stop");
171
- console.log("");
172
- const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
173
- let frameIndex = 0;
174
- let mcpEventsCount = 0;
175
- let lastEvent = "Started successfully";
176
- global.auvryntLogEmitter = (level, event) => {
177
- mcpEventsCount++;
178
- lastEvent = `${event} (${level})`;
179
- };
180
- const interval = setInterval(() => {
181
- const frame = frames[frameIndex];
182
- frameIndex = (frameIndex + 1) % frames.length;
183
- process.stdout.write(`\r\x1b[K \x1b[32m${frame}\x1b[0m Logs active... (${mcpEventsCount} requests handled | Last: ${lastEvent})`);
184
- }, 100);
185
- global.auvryntStartInterval = interval;
186
- }
187
- else {
188
- console.log(`auvrynt listening on http://${config.host}:${config.port}/mcp`);
189
- console.log(`public base url: ${config.publicBaseUrl}`);
190
- console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
191
- console.log(`allowed hosts: ${config.allowedHosts.join(", ")}`);
192
- if (config.allowedHosts.includes("*")) {
193
- console.warn("warning: Host header allowlist is disabled because AUVRYNT_ALLOWED_HOSTS=*");
230
+ await new Promise((resolveServer, rejectServer) => {
231
+ const httpServer = app.listen(config.port, config.host, () => {
232
+ if (startMode) {
233
+ console.clear();
234
+ console.log("");
235
+ console.log(" \x1b[36m\x1b[1mAuvrynt: Webkit Arsenal is ready\x1b[0m");
236
+ console.log("");
237
+ console.log(" \x1b[90mClaude Web connector URL:\x1b[0m");
238
+ console.log(" \x1b[36m" + publicMcpUrl + "\x1b[0m");
239
+ console.log("");
240
+ console.log(" \x1b[90mAuthorization page:\x1b[0m");
241
+ console.log(" \x1b[36m" + config.publicBaseUrl.replace(/\/$/, "") + "/authorize\x1b[0m");
242
+ console.log("");
243
+ console.log(" \x1b[90mOwner token:\x1b[0m");
244
+ console.log(" \x1b[33m" + ownerToken + "\x1b[0m");
245
+ console.log("");
246
+ console.log(" \x1b[90mNote:\x1b[0m");
247
+ console.log(" Web-agent workspace: " + config.allowedRoots.join(", "));
248
+ console.log(" The Cloudflare URL is temporary.");
249
+ console.log(" Recreate or edit the web agent connector after restart.");
250
+ console.log("");
251
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
252
+ let frameIndex = 0;
253
+ let mcpEventsCount = 0;
254
+ let lastEvent = "Started successfully";
255
+ global.auvryntLogEmitter = (level, event) => {
256
+ mcpEventsCount++;
257
+ lastEvent = `${event} (${level})`;
258
+ };
259
+ const interval = setInterval(() => {
260
+ const frame = frames[frameIndex];
261
+ frameIndex = (frameIndex + 1) % frames.length;
262
+ process.stdout.write(`\r\x1b[K \x1b[32m${frame}\x1b[0m Logs active... (${mcpEventsCount} requests handled | Last: ${lastEvent})`);
263
+ }, 100);
264
+ global.auvryntStartInterval = interval;
194
265
  }
195
- console.log("auth: Owner password approval required");
196
- console.log(`logging: ${config.logging.level} ${config.logging.format}`);
197
- }
266
+ else {
267
+ console.log(`auvrynt listening on http://${config.host}:${config.port}/mcp`);
268
+ console.log(`public base url: ${config.publicBaseUrl}`);
269
+ console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
270
+ console.log(`allowed hosts: ${config.allowedHosts.join(", ")}`);
271
+ if (config.allowedHosts.includes("*")) {
272
+ console.warn("warning: Host header allowlist is disabled because AUVRYNT_ALLOWED_HOSTS=*");
273
+ }
274
+ console.log("auth: Owner password approval required");
275
+ console.log(`logging: ${config.logging.level} ${config.logging.format}`);
276
+ }
277
+ });
278
+ const removeSignalHandlers = () => {
279
+ process.removeListener("SIGINT", shutdown);
280
+ process.removeListener("SIGTERM", shutdown);
281
+ };
282
+ const shutdown = () => {
283
+ if (global.auvryntStartInterval) {
284
+ clearInterval(global.auvryntStartInterval);
285
+ delete global.auvryntStartInterval;
286
+ }
287
+ httpServer.close(() => {
288
+ removeSignalHandlers();
289
+ resolveServer();
290
+ });
291
+ };
292
+ httpServer.once("error", (error) => {
293
+ removeSignalHandlers();
294
+ rejectServer(error);
295
+ });
296
+ process.once("SIGINT", shutdown);
297
+ process.once("SIGTERM", shutdown);
198
298
  });
199
- const shutdown = () => {
200
- if (global.auvryntStartInterval) {
201
- clearInterval(global.auvryntStartInterval);
202
- }
203
- httpServer.close(() => process.exit(0));
204
- };
205
- process.once("SIGINT", shutdown);
206
- process.once("SIGTERM", shutdown);
207
299
  }
208
300
  async function runDoctor() {
209
301
  const files = loadAuvryntFiles();
@@ -227,6 +319,236 @@ async function runDoctor() {
227
319
  console.log(`Config status: ${error instanceof Error ? error.message : String(error)}`);
228
320
  }
229
321
  }
322
+ async function acquireInstanceLock(stateDir) {
323
+ const lockPath = join(stateDir, "server.lock");
324
+ await mkdir(stateDir, { recursive: true });
325
+ for (let attempt = 0; attempt < 2; attempt++) {
326
+ try {
327
+ const handle = await open(lockPath, "wx");
328
+ await handle.writeFile(JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
329
+ return { release: () => releaseInstanceLock(handle, lockPath) };
330
+ }
331
+ catch (error) {
332
+ if (error.code !== "EEXIST")
333
+ throw error;
334
+ let ownerPid;
335
+ try {
336
+ const lock = JSON.parse(await readFile(lockPath, "utf8"));
337
+ ownerPid = lock.pid;
338
+ }
339
+ catch {
340
+ // A partially written lock is treated as stale and retried once.
341
+ }
342
+ if (ownerPid && isProcessRunning(ownerPid)) {
343
+ throw new Error(`Auvrynt is already running (PID ${ownerPid}). Stop that instance before starting another.`);
344
+ }
345
+ await unlink(lockPath).catch((unlinkError) => {
346
+ if (unlinkError.code !== "ENOENT")
347
+ throw unlinkError;
348
+ });
349
+ }
350
+ }
351
+ throw new Error("Could not acquire the Auvrynt server lock.");
352
+ }
353
+ function isProcessRunning(pid) {
354
+ try {
355
+ process.kill(pid, 0);
356
+ return true;
357
+ }
358
+ catch {
359
+ return false;
360
+ }
361
+ }
362
+ async function releaseInstanceLock(handle, lockPath) {
363
+ await handle.close().catch(() => undefined);
364
+ await unlink(lockPath).catch(() => undefined);
365
+ }
366
+ async function startCloudflareTunnel(port) {
367
+ const executable = await resolveCloudflaredExecutable();
368
+ const child = spawn(executable, ["tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${port}`], {
369
+ stdio: ["ignore", "pipe", "pipe"],
370
+ windowsHide: true,
371
+ });
372
+ const tunnelUrl = await new Promise((resolveUrl, reject) => {
373
+ let output = "";
374
+ const timeout = setTimeout(() => {
375
+ child.kill();
376
+ reject(new Error("Cloudflare tunnel did not provide a public URL within 30 seconds."));
377
+ }, 30_000);
378
+ const onOutput = (chunk) => {
379
+ output += chunk.toString();
380
+ const match = output.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
381
+ if (match) {
382
+ clearTimeout(timeout);
383
+ resolveUrl(match[0]);
384
+ }
385
+ };
386
+ child.stdout?.on("data", onOutput);
387
+ child.stderr?.on("data", onOutput);
388
+ child.once("error", (error) => {
389
+ clearTimeout(timeout);
390
+ reject(new Error(`Cloudflare tunnel failed to start: ${error.message}`));
391
+ });
392
+ child.once("exit", (code) => {
393
+ if (code !== null) {
394
+ clearTimeout(timeout);
395
+ reject(new Error(`Cloudflare tunnel exited before connecting (code ${code}).`));
396
+ }
397
+ });
398
+ });
399
+ return { process: child, url: tunnelUrl };
400
+ }
401
+ async function resolveCloudflaredExecutable() {
402
+ try {
403
+ return execFileSync(process.platform === "win32" ? "where.exe" : "which", ["cloudflared"], { encoding: "utf8" }).split(/\r?\n/)[0]?.trim() || "cloudflared";
404
+ }
405
+ catch {
406
+ if (process.platform === "win32") {
407
+ return installWindowsCloudflared();
408
+ }
409
+ throw new Error("cloudflared is required for `auvrynt start`. Install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/.");
410
+ }
411
+ }
412
+ function findCommand(command) {
413
+ try {
414
+ return execFileSync(process.platform === "win32" ? "where.exe" : "which", [command], { encoding: "utf8" }).split(/\r?\n/)[0]?.trim() || undefined;
415
+ }
416
+ catch {
417
+ return undefined;
418
+ }
419
+ }
420
+ async function ensureSerenaExecutable() {
421
+ const existing = findCommand("serena");
422
+ if (existing)
423
+ return existing;
424
+ const projectRoot = resolve(process.cwd());
425
+ const localCandidates = [
426
+ join(projectRoot, "serena"),
427
+ join(dirname(projectRoot), "serena"),
428
+ join(homedir(), "Desktop", "Projectsss", "serena"),
429
+ ];
430
+ const localSource = localCandidates.find((candidate) => existsSync(join(candidate, "pyproject.toml")));
431
+ const uv = await ensureUvExecutable();
432
+ console.log(localSource
433
+ ? `Serena is not installed; installing the local checkout from ${localSource}...`
434
+ : "Serena is not installed; installing the official Serena package...");
435
+ const installArgs = localSource
436
+ ? ["tool", "install", "--force", "--editable", localSource]
437
+ : ["tool", "install", "--force", "serena-agent"];
438
+ execFileSync(uv, installArgs, { stdio: "inherit" });
439
+ const installed = findCommand("serena") ?? findInstalledExecutable("serena");
440
+ if (!installed) {
441
+ throw new Error("Serena installed but its executable is not available. Restart PowerShell and run `where.exe serena`.");
442
+ }
443
+ return installed;
444
+ }
445
+ async function ensureUvExecutable() {
446
+ const existing = findCommand("uv");
447
+ if (existing)
448
+ return existing;
449
+ const python = findCommand("py") ?? findCommand("python");
450
+ if (!python) {
451
+ throw new Error("Serena requires uv, and Python was not found to install it automatically.");
452
+ }
453
+ console.log("uv is not installed; installing it for Serena...");
454
+ execFileSync(python, ["-m", "pip", "install", "--user", "uv"], { stdio: "inherit" });
455
+ const installed = findCommand("uv") ?? findInstalledExecutable("uv");
456
+ if (!installed) {
457
+ throw new Error("uv was installed but its executable is not available. Restart PowerShell and run `uv --version`.");
458
+ }
459
+ return installed;
460
+ }
461
+ function findInstalledExecutable(name) {
462
+ const executable = process.platform === "win32" ? `${name}.exe` : name;
463
+ const candidates = [
464
+ join(homedir(), ".local", "bin", executable),
465
+ join(process.env.LOCALAPPDATA ?? "", "uv", "bin", executable),
466
+ join(process.env.APPDATA ?? "", "uv", "bin", executable),
467
+ join(process.env.APPDATA ?? "", "Python", "Python313", "Scripts", executable),
468
+ join(process.env.APPDATA ?? "", "Python", "Python312", "Scripts", executable),
469
+ ];
470
+ return candidates.find((candidate) => candidate && existsSync(candidate));
471
+ }
472
+ async function installWindowsCloudflared() {
473
+ const targetDir = join(homedir(), ".auvrynt", "bin");
474
+ const executable = join(targetDir, "cloudflared.exe");
475
+ const artifact = process.arch === "arm64"
476
+ ? "cloudflared-windows-arm64.exe"
477
+ : process.arch === "ia32"
478
+ ? "cloudflared-windows-386.exe"
479
+ : "cloudflared-windows-amd64.exe";
480
+ const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/${artifact}`;
481
+ console.log("cloudflared is not installed; downloading the official Windows binary...");
482
+ const response = await fetch(downloadUrl, { signal: AbortSignal.timeout(120_000) });
483
+ if (!response.ok) {
484
+ throw new Error(`Could not download cloudflared (HTTP ${response.status}). Install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/.`);
485
+ }
486
+ await mkdir(targetDir, { recursive: true });
487
+ await writeFile(executable, Buffer.from(await response.arrayBuffer()), { mode: 0o755 });
488
+ return executable;
489
+ }
490
+ async function runStatus() {
491
+ const files = loadAuvryntFiles();
492
+ const host = files.config.host ?? "127.0.0.1";
493
+ const port = files.config.port ?? 49321;
494
+ const healthUrl = `http://${host}:${port}/healthz`;
495
+ try {
496
+ const response = await fetch(healthUrl, { signal: AbortSignal.timeout(1500) });
497
+ const body = await response.json().catch(() => ({}));
498
+ console.log(`Local MCP: ${response.ok && body.ok ? "connected" : "error"}`);
499
+ console.log(`Health URL: ${healthUrl}`);
500
+ }
501
+ catch (error) {
502
+ console.log("Local MCP: disconnected");
503
+ console.log(`Health URL: ${healthUrl}`);
504
+ console.log(`Detail: ${error instanceof Error ? error.message : String(error)}`);
505
+ }
506
+ const local = await discoverLocalIntegrations();
507
+ console.log(`Blender MCP (9876): ${local.ports.blender_lab_mcp ? "connected" : processDetected(local, "blender") ? "running, MCP unavailable" : "not detected"}${local.executables.blender ? ` (${local.executables.blender})` : ""}`);
508
+ console.log(`Godot: ${local.ports.auvrynt_godot_bridge ? "Auvrynt bridge connected" : processDetected(local, "godot") ? "running, bridge unavailable" : "not detected"}${local.executables.godot ? ` (${local.executables.godot})` : ""}`);
509
+ if (local.executables.godotCsharp)
510
+ console.log(`Godot C#: configured (${local.executables.godotCsharp})`);
511
+ console.log(`Cloudflare Tunnel: ${processDetected(local, "cloudflare_tunnel") ? "running" : local.executables.cloudflared ? "installed, not running" : "not installed"}`);
512
+ console.log(`Serena: ${processDetected(local, "serena") ? "running" : local.executables.serena ? `installed (${local.executables.serena})` : "not installed"}`);
513
+ }
514
+ function stateDirForFiles(files) {
515
+ return resolve(expandHomePath(files.config.stateDir ?? join(homedir(), ".local", "share", "auvrynt")));
516
+ }
517
+ function runConnected() {
518
+ const clients = readConnectedClients(stateDirForFiles(loadAuvryntFiles()));
519
+ console.log("Connected web agents:");
520
+ if (clients.length === 0) {
521
+ console.log(" none recorded yet");
522
+ return;
523
+ }
524
+ for (const client of clients) {
525
+ console.log(` ${client.provider} — ${client.requestCount} request(s), last seen ${client.lastSeen}`);
526
+ if (client.userAgent)
527
+ console.log(` user-agent: ${client.userAgent}`);
528
+ }
529
+ }
530
+ async function runUninstall(skipConfirmation) {
531
+ const files = loadAuvryntFiles();
532
+ if (!skipConfirmation) {
533
+ if (!input.isTTY || !output.isTTY) {
534
+ throw new Error("Uninstall is destructive in a non-interactive terminal. Re-run with `auvrynt uninstall --yes`.");
535
+ }
536
+ const answer = await prompts.confirm({ message: `Remove Auvrynt configuration from ${files.dir}?`, initialValue: false });
537
+ if (prompts.isCancel(answer) || !answer) {
538
+ console.log("Uninstall cancelled.");
539
+ return;
540
+ }
541
+ }
542
+ if (files.configExists || files.authExists) {
543
+ rmSync(files.dir, { recursive: true, force: true });
544
+ console.log(`Removed Auvrynt configuration: ${files.dir}`);
545
+ }
546
+ else {
547
+ console.log("Auvrynt configuration was already absent.");
548
+ }
549
+ console.log("The npm CLI package remains installed. Remove it with: npm uninstall -g auvrynt");
550
+ console.log("Custom state/worktree directories were preserved.");
551
+ }
230
552
  function runConfigCommand(args) {
231
553
  const [subcommand, key, ...rest] = args;
232
554
  const files = loadAuvryntFiles();
@@ -256,17 +578,106 @@ function printHelp() {
256
578
  "",
257
579
  "Usage:",
258
580
  " auvrynt Run first-time setup if needed, then start the server",
259
- " auvrynt start Start the server with an animated logs indicator (clean UI)",
581
+ " auvrynt start Start a Cloudflare tunnel scoped to the current directory",
260
582
  " auvrynt serve Start the server with verbose console logs",
261
583
  " auvrynt init Create or update ~/.auvrynt/config.json and auth.json",
584
+ " auvrynt setup Configure tool integrations (Serena, Godot, Blender...)",
262
585
  " auvrynt doctor Show config, runtime, and native dependency status",
586
+ " auvrynt status Show local MCP and integration connection status",
587
+ " auvrynt connected Show recently connected MCP/web-agent providers",
588
+ " auvrynt uninstall Remove Auvrynt configuration after confirmation",
589
+ " auvrynt uninstall -y Remove Auvrynt configuration without confirmation",
263
590
  " auvrynt config get Print persisted config",
264
591
  " auvrynt config set publicBaseUrl <url|null>",
265
592
  "",
266
593
  "For temporary tunnels:",
267
- " AUVRYNT_PUBLIC_BASE_URL=https://example.trycloudflare.com auvrynt serve",
594
+ " AUVRYNT_PUBLIC_BASE_URL=https://example.trycloudflare.com auvrynt start",
268
595
  ].join("\n"));
269
596
  }
597
+ // ─── auvrynt setup ────────────────────────────────────────────────────────────
598
+ const SETUP_TOOL_LABELS = {
599
+ serena: "Serena - semantic code search / code intelligence",
600
+ godot: "Godot - GDScript game engine",
601
+ godotCsharp: "Godot C# - .NET / Mono Godot build",
602
+ blender: "Blender - 3D modelling and rendering",
603
+ };
604
+ const SETUP_TOOL_KEYS = ["serena", "godot", "godotCsharp", "blender"];
605
+ async function runSetup() {
606
+ prompts.intro(" Auvrynt Setup - configure local tool integrations ");
607
+ const files = loadAuvryntFiles();
608
+ const existingExecs = files.config.executables ?? {};
609
+ // 1. Pick which tools to configure
610
+ const picked = await prompts.multiselect({
611
+ message: "Select tools to configure (arrows navigate, space select, enter confirm)",
612
+ options: SETUP_TOOL_KEYS.map((key) => ({
613
+ value: key,
614
+ label: SETUP_TOOL_LABELS[key],
615
+ hint: existingExecs[key] ? `currently: ${existingExecs[key]}` : undefined,
616
+ })),
617
+ required: false,
618
+ });
619
+ if (prompts.isCancel(picked)) {
620
+ prompts.cancel("Setup cancelled.");
621
+ return;
622
+ }
623
+ const selection = picked;
624
+ if (selection.length === 0) {
625
+ prompts.outro("Nothing selected - no changes made.");
626
+ return;
627
+ }
628
+ // 2. Prompt for executable path for each selected tool
629
+ const updated = { ...existingExecs };
630
+ for (const key of selection) {
631
+ const label = SETUP_TOOL_LABELS[key].split(" - ")[0].trim();
632
+ let placeholder;
633
+ switch (key) {
634
+ case "serena":
635
+ placeholder = "e.g. C:\\tools\\serena.exe or just serena if on PATH";
636
+ break;
637
+ case "godot":
638
+ placeholder = "e.g. C:\\Program Files\\Godot\\Godot.exe";
639
+ break;
640
+ case "godotCsharp":
641
+ placeholder = "e.g. C:\\Program Files\\Godot_v4-mono\\Godot.exe (.NET build)";
642
+ break;
643
+ case "blender":
644
+ placeholder = "e.g. C:\\Program Files\\Blender Foundation\\Blender 4.3\\blender.exe";
645
+ break;
646
+ default:
647
+ placeholder = "";
648
+ }
649
+ const answer = await prompts.text({
650
+ message: ` executable path`,
651
+ placeholder,
652
+ initialValue: existingExecs[key] ?? "",
653
+ validate: (val) => {
654
+ if (!(val ?? "").trim())
655
+ return "Path cannot be empty.";
656
+ return undefined;
657
+ },
658
+ });
659
+ if (prompts.isCancel(answer)) {
660
+ prompts.cancel("Setup cancelled.");
661
+ return;
662
+ }
663
+ updated[key] = answer.trim();
664
+ }
665
+ // 3. Persist to ~/.auvrynt/config.json
666
+ writeAuvryntConfig({
667
+ ...files.config,
668
+ executables: {
669
+ serena: updated.serena,
670
+ godot: updated.godot,
671
+ godotCsharp: updated.godotCsharp,
672
+ blender: updated.blender,
673
+ },
674
+ });
675
+ // 4. Show summary
676
+ prompts.note(selection
677
+ .map((key) => ` -> `)
678
+ .join("\n"), "Saved to ~/.auvrynt/config.json");
679
+ prompts.outro("Setup complete. Run `auvrynt status` to verify.");
680
+ }
270
681
  function normalizeOptionalPublicBaseUrl(value) {
271
682
  const trimmed = value.trim();
272
683
  if (!trimmed || trimmed === "null" || trimmed === "none")