aisnitch 0.2.24 → 0.2.26

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
@@ -82,10 +82,16 @@ brew install aisnitch
82
82
  ### 2. Run
83
83
 
84
84
  ```bash
85
- aisnitch start
85
+ aisnitch
86
86
  ```
87
87
 
88
- That's it! The dashboard opens with live events from all configured tools.
88
+ That's it! `aisnitch` (no argument) starts the daemon, launches the dashboard server in the background, and opens the web dashboard in your browser.
89
+
90
+ Want the in-terminal TUI instead?
91
+
92
+ ```bash
93
+ aisnitch start
94
+ ```
89
95
 
90
96
  **No AI tools yet?** Try the demo mode:
91
97
 
@@ -222,8 +228,8 @@ brew upgrade aisnitch
222
228
  Open a beautiful real-time dashboard in your browser:
223
229
 
224
230
  ```bash
225
- aisnitch fs # Open dashboard (auto-starts daemon if needed)
226
- aisnitch fs --daemon # Start daemon + open dashboard
231
+ aisnitch # Default: start daemon + dashboard + open browser
232
+ aisnitch fs # Same as `aisnitch`
227
233
  aisnitch fs --dashboard-port 8080 # Custom port
228
234
  aisnitch fs --no-browser # Just start the server
229
235
  ```
@@ -336,12 +342,12 @@ aisnitch start --type agent.coding # Filter by event type
336
342
  aisnitch start --view full-data # Show full JSON
337
343
  ```
338
344
 
339
- ### Web Dashboard
345
+ ### Web Dashboard (Default)
340
346
 
341
347
  ```bash
342
- aisnitch fs # Open web dashboard
343
- aisnitch fs --daemon # Start daemon + open
344
- aisnitch fs --dashboard-port 8080 # Custom port
348
+ aisnitch # Default: daemon + dashboard + browser
349
+ aisnitch fs # Explicit form (same effect)
350
+ aisnitch fs --dashboard-port 8080 # Custom port
345
351
  aisnitch fs --no-browser # Server only
346
352
  ```
347
353
 
@@ -771,7 +771,7 @@ var import_commander = require("commander");
771
771
 
772
772
  // src/package-info.ts
773
773
  var AISNITCH_PACKAGE_NAME = "aisnitch";
774
- var AISNITCH_VERSION = "0.2.24";
774
+ var AISNITCH_VERSION = "0.2.26";
775
775
  var AISNITCH_DESCRIPTION = "Universal bridge for AI coding tool activity \u2014 capture, normalize, stream.";
776
776
 
777
777
  // src/core/events/schema.ts
@@ -1360,6 +1360,7 @@ var AutoUpdateConfigSchema = import_zod2.z.strictObject({
1360
1360
  var ConfigSchema = import_zod2.z.strictObject({
1361
1361
  wsPort: import_zod2.z.number().int().min(1024).max(65535).default(4820),
1362
1362
  httpPort: import_zod2.z.number().int().min(1024).max(65535).default(4821),
1363
+ dashboardPort: import_zod2.z.number().int().min(1024).max(65535).default(5174),
1363
1364
  /**
1364
1365
  * 📖 This is intentionally a partial record because most users will only
1365
1366
  * override a couple of adapters instead of all supported tools at once.
@@ -1378,6 +1379,7 @@ var ConfigSchema = import_zod2.z.strictObject({
1378
1379
  var DEFAULT_CONFIG = {
1379
1380
  wsPort: 4820,
1380
1381
  httpPort: 4821,
1382
+ dashboardPort: 5174,
1381
1383
  adapters: {},
1382
1384
  autoUpdate: {
1383
1385
  enabled: true,
@@ -14168,6 +14170,10 @@ function Header({
14168
14170
  ] }) }),
14169
14171
  daemon ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
14170
14172
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_ink6.Text, { color: connected ? TUI_THEME.success : TUI_THEME.warning, children: connected ? `\u25CF ${connectionLabel}` : `\u25CB ${connectionLabel}` }),
14173
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_ink6.Text, { color: TUI_THEME.success, children: [
14174
+ "Web ",
14175
+ daemon.dashboardUrl
14176
+ ] }),
14171
14177
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_ink6.Text, { color: TUI_THEME.muted, children: [
14172
14178
  "WS ",
14173
14179
  daemon.wsUrl
@@ -14360,7 +14366,7 @@ function StatusBar({
14360
14366
  }) {
14361
14367
  const streamState = streamFrozen ? `Frozen +${pendingEventCount}` : latestEvent?.type ?? "Live";
14362
14368
  const focusLabel = focusPanel === "events" ? "events" : viewMode === "full-data" ? "inspector" : "sessions";
14363
- const daemonLabel = daemon === void 0 ? null : daemon.busyAction ? `Daemon ${daemon.busyAction}` : daemon.active ? `Daemon active \xB7 ${daemon.wsUrl}` : `Daemon not active \xB7 ${daemon.wsUrl}`;
14369
+ const daemonLabel = daemon === void 0 ? null : daemon.busyAction ? `Daemon ${daemon.busyAction}` : daemon.active ? `Daemon active \xB7 Web ${daemon.dashboardUrl}` : `Daemon not active \xB7 Web ${daemon.dashboardUrl}`;
14364
14370
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
14365
14371
  import_ink10.Box,
14366
14372
  {
@@ -15945,6 +15951,7 @@ var DAEMON_READY_TIMEOUT_MS = 4e3;
15945
15951
  var DAEMON_READY_POLL_INTERVAL_MS = 100;
15946
15952
  var DAEMON_STOP_TIMEOUT_MS = 4e3;
15947
15953
  var DAEMON_LOG_MAX_BYTES = 5 * 1024 * 1024;
15954
+ var DEFAULT_DASHBOARD_PORT = 5174;
15948
15955
  var LAUNCH_AGENT_LABEL = "com.aisnitch.daemon";
15949
15956
  async function resolveNodeExecutable() {
15950
15957
  try {
@@ -15960,6 +15967,40 @@ function formatSpawnError(error) {
15960
15967
  }
15961
15968
  return String(error);
15962
15969
  }
15970
+ async function pathExists(path2) {
15971
+ try {
15972
+ await (0, import_promises22.access)(path2, import_node_fs7.constants.R_OK);
15973
+ return true;
15974
+ } catch {
15975
+ return false;
15976
+ }
15977
+ }
15978
+ async function resolveDashboardDistPath() {
15979
+ if (process.env.AISNITCH_DASHBOARD_DIST) {
15980
+ const overridePath = process.env.AISNITCH_DASHBOARD_DIST;
15981
+ if (await pathExists((0, import_node_path22.join)(overridePath, "index.html"))) {
15982
+ return overridePath;
15983
+ }
15984
+ throw new Error(
15985
+ `Fullscreen dashboard assets are missing at ${overridePath}. Reinstall AISnitch or run \`pnpm --filter aisnitch-fullscreen-dashboard build\` from the repository checkout.`
15986
+ );
15987
+ }
15988
+ const cliEntryPath = process.argv[1] ? await (0, import_promises22.realpath)(process.argv[1]).catch(() => process.argv[1] ?? "") : "";
15989
+ const moduleDirectory = (0, import_node_path22.dirname)(cliEntryPath);
15990
+ const packageRoot = (0, import_node_path22.dirname)((0, import_node_path22.dirname)(moduleDirectory));
15991
+ const candidates = [
15992
+ (0, import_node_path22.join)(packageRoot, "examples", "fullscreen-dashboard", "dist"),
15993
+ (0, import_node_path22.join)(process.cwd(), "examples", "fullscreen-dashboard", "dist")
15994
+ ];
15995
+ for (const candidate of candidates) {
15996
+ if (await pathExists((0, import_node_path22.join)(candidate, "index.html"))) {
15997
+ return candidate;
15998
+ }
15999
+ }
16000
+ throw new Error(
16001
+ "Fullscreen dashboard assets are missing. Reinstall AISnitch or run `pnpm --filter aisnitch-fullscreen-dashboard build` from the repository checkout."
16002
+ );
16003
+ }
15963
16004
  function createCliRuntime(dependencies = {}) {
15964
16005
  const output = dependencies.output ?? createProcessOutput();
15965
16006
  const fetchImplementation = dependencies.fetch ?? globalThis.fetch;
@@ -15985,6 +16026,7 @@ function createCliRuntime(dependencies = {}) {
15985
16026
  const config = ConfigSchema.parse({
15986
16027
  ...baseConfig,
15987
16028
  ...isStartCliOptions(options) ? {
16029
+ dashboardPort: options.dashboardPort ?? baseConfig.dashboardPort,
15988
16030
  httpPort: options.httpPort ?? baseConfig.httpPort,
15989
16031
  logLevel: options.logLevel ?? baseConfig.logLevel,
15990
16032
  wsPort: options.wsPort ?? baseConfig.wsPort
@@ -16008,6 +16050,149 @@ function createCliRuntime(dependencies = {}) {
16008
16050
  return null;
16009
16051
  }
16010
16052
  }
16053
+ async function fetchOk(url) {
16054
+ try {
16055
+ const response = await fetchImplementation(url);
16056
+ return response.ok;
16057
+ } catch {
16058
+ return false;
16059
+ }
16060
+ }
16061
+ async function waitForDashboardReady(port) {
16062
+ const dashboardUrl = `http://127.0.0.1:${port}`;
16063
+ for (let i = 0; i < 100; i++) {
16064
+ if (await fetchOk(dashboardUrl)) {
16065
+ return true;
16066
+ }
16067
+ await sleep(100);
16068
+ }
16069
+ return false;
16070
+ }
16071
+ async function startDashboardServerProcess(port) {
16072
+ const dashboardUrl = `http://127.0.0.1:${port}`;
16073
+ if (await fetchOk(dashboardUrl)) {
16074
+ return {
16075
+ kill: () => void 0,
16076
+ pid: void 0
16077
+ };
16078
+ }
16079
+ const distPath = await resolveDashboardDistPath();
16080
+ const nodeExecutable = await resolveNodeExecutable();
16081
+ const serverProcess = spawnImplementation(nodeExecutable, [
16082
+ "-e",
16083
+ buildDashboardServerScript(distPath, port)
16084
+ ], {
16085
+ cwd: distPath,
16086
+ stdio: ["ignore", "pipe", "pipe"]
16087
+ });
16088
+ let serverOutput = "";
16089
+ let serverSpawnError;
16090
+ serverProcess.on("error", (error) => {
16091
+ serverSpawnError = error;
16092
+ serverOutput += `Dashboard server process failed: ${formatSpawnError(error)}
16093
+ `;
16094
+ });
16095
+ serverProcess.stdout?.on("data", (data) => {
16096
+ serverOutput += data.toString();
16097
+ });
16098
+ serverProcess.stderr?.on("data", (data) => {
16099
+ serverOutput += data.toString();
16100
+ });
16101
+ for (let i = 0; i < 100; i++) {
16102
+ if (await fetchOk(dashboardUrl)) {
16103
+ return {
16104
+ kill: () => {
16105
+ if (serverProcess.pid !== void 0) {
16106
+ try {
16107
+ process.kill(serverProcess.pid, "SIGTERM");
16108
+ } catch {
16109
+ }
16110
+ }
16111
+ },
16112
+ pid: serverProcess.pid
16113
+ };
16114
+ }
16115
+ if (serverSpawnError !== void 0) {
16116
+ throw new Error(
16117
+ `Failed to start dashboard server process with ${nodeExecutable}: ${formatSpawnError(serverSpawnError)}`
16118
+ );
16119
+ }
16120
+ await sleep(100);
16121
+ }
16122
+ throw new Error(
16123
+ `Failed to start dashboard server at ${dashboardUrl}. Server output: ${serverOutput}`
16124
+ );
16125
+ }
16126
+ function buildDashboardServerScript(distPath, port) {
16127
+ return `
16128
+ import { createReadStream } from 'node:fs';
16129
+ import { stat } from 'node:fs/promises';
16130
+ import { createServer } from 'node:http';
16131
+ import { extname, join, normalize, resolve } from 'node:path';
16132
+
16133
+ const distPath = ${JSON.stringify(distPath)};
16134
+ const port = ${port};
16135
+ const root = resolve(distPath);
16136
+ const contentTypes = new Map([
16137
+ ['.css', 'text/css; charset=utf-8'],
16138
+ ['.html', 'text/html; charset=utf-8'],
16139
+ ['.js', 'text/javascript; charset=utf-8'],
16140
+ ['.json', 'application/json; charset=utf-8'],
16141
+ ['.map', 'application/json; charset=utf-8'],
16142
+ ['.svg', 'image/svg+xml'],
16143
+ ]);
16144
+
16145
+ function safePath(url) {
16146
+ const parsed = new URL(url ?? '/', 'http://127.0.0.1');
16147
+ const pathname = parsed.pathname === '/' ? '/index.html' : parsed.pathname;
16148
+ const decoded = decodeURIComponent(pathname);
16149
+ const normalized = normalize(decoded).replace(/^[/\\]+/, '');
16150
+ const absolute = resolve(join(root, normalized));
16151
+
16152
+ if (absolute !== root && !absolute.startsWith(root + '/')) {
16153
+ return null;
16154
+ }
16155
+
16156
+ return absolute;
16157
+ }
16158
+
16159
+ await stat(join(root, 'index.html'));
16160
+
16161
+ const server = createServer(async (request, response) => {
16162
+ const requestedPath = safePath(request.url);
16163
+
16164
+ if (requestedPath === null) {
16165
+ response.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
16166
+ response.end('Forbidden');
16167
+ return;
16168
+ }
16169
+
16170
+ try {
16171
+ const fileStat = await stat(requestedPath);
16172
+ const filePath = fileStat.isFile() ? requestedPath : join(root, 'index.html');
16173
+ const contentType = contentTypes.get(extname(filePath)) ?? 'application/octet-stream';
16174
+
16175
+ response.writeHead(200, { 'content-type': contentType });
16176
+ createReadStream(filePath).pipe(response);
16177
+ } catch {
16178
+ response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
16179
+ createReadStream(join(root, 'index.html')).pipe(response);
16180
+ }
16181
+ });
16182
+
16183
+ server.on('error', (error) => {
16184
+ console.error(error instanceof Error ? error.message : String(error));
16185
+ process.exitCode = 1;
16186
+ });
16187
+
16188
+ await new Promise((resolveListen, rejectListen) => {
16189
+ server.once('error', rejectListen);
16190
+ server.listen(port, '127.0.0.1', resolveListen);
16191
+ });
16192
+
16193
+ process.stdin.resume();
16194
+ `;
16195
+ }
16011
16196
  async function ensureDaemonNotRunning(options) {
16012
16197
  const pathOptions = toPathOptions2(options);
16013
16198
  await cleanupStaleDaemonFiles(pathOptions);
@@ -16041,7 +16226,10 @@ function createCliRuntime(dependencies = {}) {
16041
16226
  if (child.pid === void 0) {
16042
16227
  throw new Error("Failed to obtain the AISnitch daemon PID.");
16043
16228
  }
16044
- return await waitForDaemonReady(daemonPathOptions);
16229
+ return await waitForDaemonReady(
16230
+ daemonPathOptions,
16231
+ options.dashboardPort ?? DEFAULT_DASHBOARD_PORT
16232
+ );
16045
16233
  }
16046
16234
  async function stopDetachedDaemon(options) {
16047
16235
  const pathOptions = toPathOptions2(options);
@@ -16072,6 +16260,7 @@ function createCliRuntime(dependencies = {}) {
16072
16260
  consumerCount: snapshot.health?.consumers ?? 0,
16073
16261
  daemon: {
16074
16262
  active: snapshot.running,
16263
+ dashboardUrl: `http://127.0.0.1:${snapshot.dashboardPort}`,
16075
16264
  httpUrl: `http://127.0.0.1:${snapshot.httpPort}/health`,
16076
16265
  pid: snapshot.daemonPid,
16077
16266
  socketPath: snapshot.socketPath,
@@ -16112,7 +16301,7 @@ function createCliRuntime(dependencies = {}) {
16112
16301
  await (0, import_promises22.rm)(backupPath, { force: true });
16113
16302
  await (0, import_promises22.rename)(logFilePath, backupPath);
16114
16303
  }
16115
- async function waitForDaemonReady(pathOptions) {
16304
+ async function waitForDaemonReady(pathOptions, dashboardPort = DEFAULT_DASHBOARD_PORT) {
16116
16305
  const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
16117
16306
  while (Date.now() < deadline) {
16118
16307
  const daemonState = await readDaemonState(pathOptions);
@@ -16121,6 +16310,7 @@ function createCliRuntime(dependencies = {}) {
16121
16310
  if (health !== null) {
16122
16311
  return {
16123
16312
  configuredAdapters: [],
16313
+ dashboardPort,
16124
16314
  daemonPid: daemonState.pid,
16125
16315
  health,
16126
16316
  httpPort: daemonState.httpPort,
@@ -16221,6 +16411,7 @@ function createCliRuntime(dependencies = {}) {
16221
16411
  ...pathOptions
16222
16412
  });
16223
16413
  startMockEmitter(pipeline, options);
16414
+ const dashboardServer = daemonMode ? await startDashboardServerProcess(config.dashboardPort) : null;
16224
16415
  if (daemonMode) {
16225
16416
  const daemonPathOptions = toPathOptions2(options);
16226
16417
  await writePid(process.pid, daemonPathOptions);
@@ -16257,6 +16448,7 @@ function createCliRuntime(dependencies = {}) {
16257
16448
  wsServer: pipeline.getWsServer(),
16258
16449
  eventBus: pipeline.getEventBus(),
16259
16450
  cleanupFns: daemonMode ? [async () => {
16451
+ dashboardServer?.kill();
16260
16452
  await Promise.all([
16261
16453
  removePid(toPathOptions2(options)),
16262
16454
  removeDaemonState(toPathOptions2(options))
@@ -16333,16 +16525,16 @@ function createCliRuntime(dependencies = {}) {
16333
16525
  const snapshot = await startDetachedDaemon(options);
16334
16526
  output.stdout(
16335
16527
  `AISnitch daemon started (PID: ${snapshot.daemonPid ?? "unknown"}) on ws://${"127.0.0.1"}:${snapshot.wsPort}
16528
+ Dashboard: http://127.0.0.1:${snapshot.dashboardPort}
16336
16529
  `
16337
16530
  );
16338
16531
  return;
16339
16532
  }
16340
16533
  let initialSnapshot = await getStatusSnapshot(options);
16534
+ if (!initialSnapshot.running) {
16535
+ initialSnapshot = await startDetachedDaemon(options);
16536
+ }
16341
16537
  if (options.mock) {
16342
- if (!initialSnapshot.running) {
16343
- await startDetachedDaemon(options);
16344
- initialSnapshot = await getStatusSnapshot(options);
16345
- }
16346
16538
  output.stdout(
16347
16539
  `Starting mock scenario ${options.mock} (${options.mockSpeed ?? 1}x, ${options.mockDuration ?? 60}s${options.mockLoop ? ", loop" : ""}).
16348
16540
  `
@@ -16402,6 +16594,8 @@ function createCliRuntime(dependencies = {}) {
16402
16594
  output.stdout(`WebSocket port: ${snapshot.wsPort}
16403
16595
  `);
16404
16596
  output.stdout(`HTTP port: ${snapshot.httpPort}
16597
+ `);
16598
+ output.stdout(`Dashboard: http://127.0.0.1:${snapshot.dashboardPort}
16405
16599
  `);
16406
16600
  output.stdout(`Socket path: ${snapshot.socketPath ?? "none"}
16407
16601
  `);
@@ -16458,126 +16652,49 @@ function createCliRuntime(dependencies = {}) {
16458
16652
  });
16459
16653
  }
16460
16654
  async function fullscreen(options) {
16461
- const snapshot = await getStatusSnapshot(options);
16462
- if (!snapshot.running && options.daemon) {
16655
+ let snapshot = await getStatusSnapshot(options);
16656
+ if (!snapshot.running) {
16463
16657
  output.stdout("Starting daemon...\n");
16464
- await startDetachedDaemon(options);
16465
- }
16466
- if (!snapshot.running && !options.daemon) {
16467
- throw new Error(
16468
- "AISnitch daemon is not running. Start one with `aisnitch start --daemon` or use `aisnitch fs --daemon` to start and open the dashboard."
16469
- );
16470
- }
16471
- for (let i = 0; i < 20; i++) {
16472
- const health = await fetchHealth(snapshot.httpPort);
16473
- if (health) break;
16474
- await new Promise((resolve2) => setTimeout(resolve2, 200).unref());
16658
+ snapshot = await startDetachedDaemon({
16659
+ ...options,
16660
+ dashboardPort: options.dashboardPort
16661
+ });
16475
16662
  }
16476
- const dashboardPort = options.dashboardPort ?? 5174;
16663
+ const dashboardPort = options.dashboardPort ?? snapshot.dashboardPort;
16477
16664
  const dashboardUrl = `http://127.0.0.1:${dashboardPort}`;
16478
- const distPath = (0, import_node_path22.join)(process.cwd(), "examples", "fullscreen-dashboard", "dist");
16479
- output.stdout(`Starting dashboard server on port ${dashboardPort}...
16480
- `);
16481
- const nodeExecutable = await resolveNodeExecutable();
16482
- const viteProcess = spawnImplementation(nodeExecutable, [
16483
- "-e",
16484
- `
16485
- import { createServer } from 'vite';
16486
- import path from 'path';
16487
-
16488
- const distPath = '${distPath}';
16489
- const port = ${dashboardPort};
16490
-
16491
- const server = await createServer({
16492
- root: distPath,
16493
- server: {
16494
- port,
16495
- strictPort: true,
16496
- allowedHosts: true,
16497
- },
16498
- preview: {
16499
- port,
16500
- strictPort: true,
16501
- },
16502
- });
16503
-
16504
- await server.listen();
16505
- console.log('READY');
16506
-
16507
- process.stdin.resume();
16665
+ if (!await waitForDashboardReady(dashboardPort)) {
16666
+ output.stdout(
16667
+ `Dashboard not reachable at ${dashboardUrl}, starting a standalone server...
16508
16668
  `
16509
- ], {
16510
- cwd: distPath,
16511
- stdio: ["pipe", "pipe", "pipe"]
16512
- });
16513
- let serverOutput = "";
16514
- let serverSpawnError;
16515
- viteProcess.on("error", (error) => {
16516
- serverSpawnError = error;
16517
- serverOutput += `Dashboard server process failed: ${formatSpawnError(error)}
16518
- `;
16519
- });
16520
- viteProcess.stdout?.on("data", (data) => {
16521
- serverOutput += data.toString();
16522
- });
16523
- viteProcess.stderr?.on("data", (data) => {
16524
- serverOutput += data.toString();
16525
- });
16526
- let serverReady = false;
16527
- for (let i = 0; i < 100; i++) {
16528
- await new Promise((resolve2) => setTimeout(resolve2, 100).unref());
16669
+ );
16529
16670
  try {
16530
- const response = await fetchImplementation(dashboardUrl);
16531
- if (response.ok) {
16532
- serverReady = true;
16533
- break;
16534
- }
16535
- } catch {
16536
- }
16537
- if (serverSpawnError !== void 0) {
16538
- break;
16671
+ await startDashboardServerProcess(dashboardPort);
16672
+ } catch (error) {
16673
+ throw new Error(
16674
+ `Dashboard did not become reachable at ${dashboardUrl}: ${error instanceof Error ? error.message : "unknown error"}`,
16675
+ { cause: error }
16676
+ );
16539
16677
  }
16540
- if (!viteProcess.pid) break;
16541
- }
16542
- if (!serverReady) {
16543
- output.stdout(`Server output: ${serverOutput}
16544
- `);
16545
- if (serverSpawnError !== void 0) {
16678
+ if (!await waitForDashboardReady(dashboardPort)) {
16546
16679
  throw new Error(
16547
- `Failed to start dashboard server process with ${nodeExecutable}: ${formatSpawnError(serverSpawnError)}`
16680
+ `Dashboard did not become reachable at ${dashboardUrl}.`
16548
16681
  );
16549
16682
  }
16550
- throw new Error("Failed to start dashboard server");
16551
16683
  }
16552
16684
  output.stdout(`Dashboard ready at ${dashboardUrl}
16553
16685
  `);
16554
16686
  if (!options.noBrowser) {
16555
16687
  output.stdout("Opening browser...\n");
16556
- const openModule = await openPromise;
16557
- await openModule.default(dashboardUrl);
16688
+ try {
16689
+ const openModule = await openPromise;
16690
+ await openModule.default(dashboardUrl);
16691
+ } catch (error) {
16692
+ output.stderr(
16693
+ `Failed to open browser automatically (${error instanceof Error ? error.message : "unknown error"}). Open ${dashboardUrl} manually.
16694
+ `
16695
+ );
16696
+ }
16558
16697
  }
16559
- output.stdout("Press Ctrl+C to stop\n");
16560
- await new Promise((resolve2) => {
16561
- const shutdown = () => {
16562
- process.off("SIGINT", handleSigint);
16563
- process.off("SIGTERM", handleSigterm);
16564
- if (viteProcess.pid !== void 0) {
16565
- try {
16566
- process.kill(viteProcess.pid, "SIGTERM");
16567
- } catch {
16568
- }
16569
- }
16570
- resolve2();
16571
- };
16572
- const handleSigint = () => {
16573
- shutdown();
16574
- };
16575
- const handleSigterm = () => {
16576
- shutdown();
16577
- };
16578
- process.once("SIGINT", handleSigint);
16579
- process.once("SIGTERM", handleSigterm);
16580
- });
16581
16698
  }
16582
16699
  async function logger2(options) {
16583
16700
  const snapshot = await getStatusSnapshot(options);
@@ -16844,6 +16961,7 @@ process.stdin.resume();
16844
16961
  const health = running && daemonState !== null ? await fetchHealth(daemonState.httpPort) : null;
16845
16962
  return {
16846
16963
  configuredAdapters: getEnabledAdapters(config),
16964
+ dashboardPort: config.dashboardPort ?? DEFAULT_DASHBOARD_PORT,
16847
16965
  daemonPid,
16848
16966
  health,
16849
16967
  httpPort: daemonState?.httpPort ?? config.httpPort,
@@ -17078,13 +17196,13 @@ function createProgram(dependencies = {}) {
17078
17196
  "after",
17079
17197
  `
17080
17198
  Examples:
17081
- aisnitch start
17199
+ aisnitch (default: starts daemon + dashboard server, opens browser)
17200
+ aisnitch fs --dashboard-port 8080
17201
+ aisnitch fs --no-browser
17202
+ aisnitch start (TUI dashboard)
17082
17203
  aisnitch start --daemon
17083
17204
  aisnitch start --view full-data
17084
17205
  aisnitch start --mock
17085
- aisnitch fs
17086
- aisnitch fs --daemon
17087
- aisnitch fs --dashboard-port 8080
17088
17206
  aisnitch status
17089
17207
  aisnitch attach
17090
17208
  aisnitch attach --view full-data
@@ -17127,7 +17245,7 @@ function addCommonOptions(command) {
17127
17245
  }
17128
17246
  function addStartCommand(program, runtime) {
17129
17247
  addCommonOptions(
17130
- program.command("start", { isDefault: true }).description("Open the AISnitch dashboard and manage the daemon").option("--daemon", "Run AISnitch as a detached daemon").option(
17248
+ program.command("start").description("Open the AISnitch TUI dashboard and manage the daemon").option("--daemon", "Run AISnitch as a detached daemon").option(
17131
17249
  "--mock [tool]",
17132
17250
  'Inject deterministic mock events (defaults to "all" when no tool is specified)',
17133
17251
  wrapOptionParser(parseMockToolSelection)
@@ -17159,6 +17277,10 @@ function addStartCommand(program, runtime) {
17159
17277
  "--http-port <port>",
17160
17278
  "Override the HTTP hook port",
17161
17279
  wrapOptionParser(parsePortOption)
17280
+ ).option(
17281
+ "--dashboard-port <port>",
17282
+ "Override the web dashboard port",
17283
+ wrapOptionParser(parsePortOption)
17162
17284
  ).option(
17163
17285
  "--log-level <level>",
17164
17286
  "Override the runtime log level",
@@ -17243,9 +17365,9 @@ function addAttachCommand(program, runtime) {
17243
17365
  }
17244
17366
  function addFullscreenCommand(program, runtime) {
17245
17367
  addCommonOptions(
17246
- program.command("fs").description("Open the fullscreen web dashboard in browser (starts daemon if needed with --daemon)").alias("fullscreen").option(
17368
+ program.command("fs", { isDefault: true }).description("Start the daemon, the dashboard server, and open the web dashboard in browser").alias("fullscreen").option(
17247
17369
  "--daemon",
17248
- "Start the daemon automatically if not running"
17370
+ "Deprecated: daemon is now started automatically when needed"
17249
17371
  ).option(
17250
17372
  "--dashboard-port <port>",
17251
17373
  "Port for the dashboard server (default: 5174)",