@townco/cli 0.1.127 → 0.1.129

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.
@@ -8,6 +8,7 @@ declare const _default: {
8
8
  readonly prompt: string | undefined;
9
9
  readonly port: number | undefined;
10
10
  readonly noSession: true | undefined;
11
+ readonly proxy: true | undefined;
11
12
  }, ["matched", string] | ["parsing", {
12
13
  readonly command: "run";
13
14
  readonly name: import("@optique/core").ValueParserResult<string> | undefined;
@@ -17,6 +18,7 @@ declare const _default: {
17
18
  readonly prompt: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
18
19
  readonly port: [import("@optique/core").ValueParserResult<number> | undefined] | undefined;
19
20
  readonly noSession: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
21
+ readonly proxy: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
20
22
  }] | undefined>;
21
23
  impl: (def: {
22
24
  readonly command: "run";
@@ -27,6 +29,7 @@ declare const _default: {
27
29
  readonly prompt: string | undefined;
28
30
  readonly port: number | undefined;
29
31
  readonly noSession: true | undefined;
32
+ readonly proxy: true | undefined;
30
33
  }) => unknown;
31
34
  };
32
35
  export default _default;
@@ -12,14 +12,16 @@ export default createCommand({
12
12
  prompt: optional(argument(string({ metavar: "PROMPT" }))),
13
13
  port: optional(option("-p", "--port", integer())),
14
14
  noSession: optional(flag("--no-session")),
15
+ proxy: optional(flag("--proxy")),
15
16
  }), { brief: message `Run an agent.` }),
16
- impl: async ({ name, http, gui, cli, prompt, port, noSession }) => {
17
+ impl: async ({ name, http, gui, cli, prompt, port, noSession, proxy }) => {
17
18
  const options = {
18
19
  name,
19
20
  http: http === true,
20
21
  gui: gui === true,
21
22
  cli: cli === true,
22
23
  noSession: noSession === true,
24
+ proxy: proxy === true || process.env.TOWN_RUN_PROXY === "true",
23
25
  };
24
26
  if (prompt !== null && prompt !== undefined) {
25
27
  options.prompt = prompt;
@@ -6,5 +6,6 @@ export interface RunCommandOptions {
6
6
  prompt?: string;
7
7
  port?: number;
8
8
  noSession?: boolean;
9
+ proxy?: boolean;
9
10
  }
10
11
  export declare function runCommand(options: RunCommandOptions): Promise<void>;
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
13
13
  import { LogsPane } from "../components/LogsPane.js";
14
14
  import { TabbedOutput } from "../components/TabbedOutput.js";
15
15
  import { ensurePortAvailable } from "../lib/port-utils.js";
16
+ import { createProxyServer } from "../lib/proxy-server.js";
16
17
  function TuiRunner({ agentPath, workingDir, agentName, noSession, onExit, }) {
17
18
  const [client, setClient] = useState(null);
18
19
  const [error, setError] = useState(null);
@@ -82,15 +83,15 @@ function TuiRunner({ agentPath, workingDir, agentName, noSession, onExit, }) {
82
83
  ], [client, error, workingDir]);
83
84
  return (_jsx(TabbedOutput, { processes: [], customTabs: customTabs, onExit: onExit }));
84
85
  }
85
- function GuiRunner({ agentProcess, guiProcess, agentPort, agentPath, logger, onExit, }) {
86
+ function GuiRunner({ agentProcess, guiProcess, agentPort, browserPort, agentPath, logger, onExit, }) {
86
87
  const browserOpenedRef = useRef(false);
87
- const handlePortDetected = useCallback((processIndex, port) => {
88
+ const handlePortDetected = useCallback((processIndex, _port) => {
88
89
  // Process index 1 is the GUI process
89
90
  if (processIndex === 1) {
90
- // Open browser once we know the actual port
91
+ // Open browser once we know the GUI is ready
91
92
  if (!browserOpenedRef.current) {
92
93
  browserOpenedRef.current = true;
93
- const guiUrl = `http://localhost:${port}`;
94
+ const guiUrl = `http://localhost:${browserPort}`;
94
95
  logger.info("Opening browser", { url: guiUrl });
95
96
  open(guiUrl).catch((error) => {
96
97
  logger.warn("Could not automatically open browser", {
@@ -100,7 +101,7 @@ function GuiRunner({ agentProcess, guiProcess, agentPort, agentPath, logger, onE
100
101
  });
101
102
  }
102
103
  }
103
- }, [logger]);
104
+ }, [logger, browserPort]);
104
105
  // Memoize processes array based only on actual process objects and initial ports
105
106
  // Don't include guiPort as dependency to prevent re-creating array when port is detected
106
107
  // TabbedOutput will update the displayed port internally via onPortDetected callback
@@ -374,7 +375,7 @@ async function runCliMode(options) {
374
375
  }
375
376
  }
376
377
  export async function runCommand(options) {
377
- const { name, http = false, gui = false, cli = false, prompt, port = 3100, noSession = false, } = options;
378
+ const { name, http = false, gui = false, cli = false, prompt, port = 3100, noSession = false, proxy = false, } = options;
378
379
  // Check if we're inside a Town project
379
380
  const projectRoot = await isInsideTownProject();
380
381
  if (projectRoot === null) {
@@ -512,9 +513,15 @@ export async function runCommand(options) {
512
513
  }
513
514
  // Ensure agent and GUI ports are available (debugger ports already checked above)
514
515
  const guiPort = 5173;
516
+ const proxyPort = process.env.TOWN_AGENT_PROXY_PORT
517
+ ? Number.parseInt(process.env.TOWN_AGENT_PROXY_PORT, 10)
518
+ : 3500;
515
519
  try {
516
520
  await ensurePortAvailable(port, "agent HTTP server");
517
521
  await ensurePortAvailable(guiPort, "GUI dev server");
522
+ if (proxy) {
523
+ await ensurePortAvailable(proxyPort, "reverse proxy");
524
+ }
518
525
  }
519
526
  catch (error) {
520
527
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -524,10 +531,18 @@ export async function runCommand(options) {
524
531
  logger.info("Starting GUI mode", {
525
532
  agentPort: port,
526
533
  guiPort,
534
+ ...(proxy ? { proxyPort } : {}),
527
535
  });
528
536
  console.log(`Starting agent "${name}" with GUI...`);
529
- console.log(`Agent HTTP server will run on port ${port}`);
530
- console.log(`GUI dev server will run on port ${guiPort}\n`);
537
+ if (proxy) {
538
+ console.log(`Proxy server running on port ${proxyPort}`);
539
+ console.log(` - GUI: http://localhost:${proxyPort}/`);
540
+ console.log(` - Agent API: http://localhost:${proxyPort}/rpc\n`);
541
+ }
542
+ else {
543
+ console.log(`GUI: http://localhost:${guiPort}/`);
544
+ console.log(`Agent API: http://localhost:${port}/rpc\n`);
545
+ }
531
546
  // Set stdin to raw mode for Ink
532
547
  if (process.stdin.isTTY) {
533
548
  process.stdin.setRawMode(true);
@@ -551,13 +566,17 @@ export async function runCommand(options) {
551
566
  if (process.env.BIND_HOST) {
552
567
  viteArgs.push("--host", process.env.BIND_HOST);
553
568
  }
569
+ // When proxy is enabled, GUI talks to proxy; otherwise directly to agent
570
+ const agentUrl = proxy
571
+ ? `http://localhost:${proxyPort}`
572
+ : `http://localhost:${port}`;
554
573
  const guiProcess = spawn("bunx", viteArgs, {
555
574
  cwd: guiPath,
556
575
  stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
557
576
  env: {
558
577
  ...process.env,
559
578
  ...configEnvVars,
560
- VITE_AGENT_URL: `${process.env.EXT_HOST || "http://localhost"}:${port}`,
579
+ VITE_AGENT_URL: process.env.EXT_HOST ?? agentUrl,
561
580
  VITE_DEBUGGER_URL: `http://localhost:${debuggerUiPort}`,
562
581
  // If agent uses library MCP, pass LIBRARY_API_URL to GUI for auth
563
582
  ...(usesLibraryMcp &&
@@ -567,12 +586,32 @@ export async function runCommand(options) {
567
586
  }),
568
587
  },
569
588
  });
570
- // Setup cleanup handlers for agent and GUI processes
589
+ // Start the reverse proxy server if enabled
590
+ const proxyServer = proxy
591
+ ? createProxyServer({
592
+ port: proxyPort,
593
+ vitePort: guiPort,
594
+ agentPort: port,
595
+ basicAuthUser: process.env.BASIC_AUTH_USER,
596
+ basicAuthPass: process.env.BASIC_AUTH_PASS,
597
+ logger,
598
+ })
599
+ : null;
600
+ // Setup cleanup handlers for agent, GUI, and proxy processes
571
601
  let isCleaningUp = false;
572
602
  const cleanupProcesses = () => {
573
603
  if (isCleaningUp)
574
604
  return;
575
605
  isCleaningUp = true;
606
+ // Stop proxy server if running
607
+ if (proxyServer) {
608
+ try {
609
+ proxyServer.stop();
610
+ }
611
+ catch (_e) {
612
+ // Proxy may already be stopped
613
+ }
614
+ }
576
615
  // Kill child processes - since they're not detached, they're in our process group
577
616
  try {
578
617
  agentProcess.kill("SIGTERM");
@@ -600,7 +639,9 @@ export async function runCommand(options) {
600
639
  cleanupDebugger?.();
601
640
  };
602
641
  // Render the tabbed UI with dynamic port detection
603
- const { waitUntilExit } = render(_jsx(GuiRunner, { agentProcess: agentProcess, guiProcess: guiProcess, agentPort: port, agentPath: agentPath, logger: logger, onExit: () => {
642
+ // Browser opens to proxy port if enabled, otherwise to GUI port
643
+ const browserPort = proxy ? proxyPort : guiPort;
644
+ const { waitUntilExit } = render(_jsx(GuiRunner, { agentProcess: agentProcess, guiProcess: guiProcess, agentPort: port, browserPort: browserPort, agentPath: agentPath, logger: logger, onExit: () => {
604
645
  cleanupProcesses();
605
646
  } }));
606
647
  // Register signal handlers AFTER Ink render to ensure they take precedence
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.127",
3
+ "version": "0.1.129",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "town": "./dist/index.js"
@@ -15,22 +15,23 @@
15
15
  "build": "tsgo"
16
16
  },
17
17
  "devDependencies": {
18
- "@townco/tsconfig": "0.1.119",
18
+ "@townco/tsconfig": "0.1.121",
19
19
  "@types/archiver": "^7.0.0",
20
20
  "@types/bun": "^1.3.1",
21
+ "@types/http-proxy": "^1.17.14",
21
22
  "@types/ignore-walk": "^4.0.3",
22
23
  "@types/react": "^19.2.2"
23
24
  },
24
25
  "dependencies": {
25
26
  "@optique/core": "^0.6.2",
26
27
  "@optique/run": "^0.6.2",
27
- "@townco/agent": "0.1.130",
28
- "@townco/apiclient": "0.0.42",
29
- "@townco/core": "0.0.100",
30
- "@townco/debugger": "0.1.78",
31
- "@townco/env": "0.1.72",
32
- "@townco/secret": "0.1.122",
33
- "@townco/ui": "0.1.122",
28
+ "@townco/agent": "0.1.132",
29
+ "@townco/apiclient": "0.0.44",
30
+ "@townco/core": "0.0.102",
31
+ "@townco/debugger": "0.1.80",
32
+ "@townco/env": "0.1.74",
33
+ "@townco/secret": "0.1.124",
34
+ "@townco/ui": "0.1.124",
34
35
  "@trpc/client": "^11.7.2",
35
36
  "archiver": "^7.0.1",
36
37
  "eventsource": "^4.1.0",
@@ -41,6 +42,7 @@
41
42
  "open": "^10.2.0",
42
43
  "superjson": "^2.2.5",
43
44
  "ts-pattern": "^5.9.0",
45
+ "http-proxy": "^1.18.1",
44
46
  "zod": "^4.1.12"
45
47
  },
46
48
  "peerDependencies": {