aisnitch 0.2.3 → 0.2.4

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/dist/cli/index.js CHANGED
@@ -3220,8 +3220,16 @@ var EventBus = class {
3220
3220
  },
3221
3221
  "Published event"
3222
3222
  );
3223
- this.emitter.emit("event", parsedEvent.data);
3224
- this.emitter.emit(`event:${parsedEvent.data.type}`, parsedEvent.data);
3223
+ try {
3224
+ this.emitter.emit("event", parsedEvent.data);
3225
+ } catch (error) {
3226
+ logger.warn({ error, eventType: parsedEvent.data.type }, "\u{1F4D6} Error in EventBus global subscriber");
3227
+ }
3228
+ try {
3229
+ this.emitter.emit(`event:${parsedEvent.data.type}`, parsedEvent.data);
3230
+ } catch (error) {
3231
+ logger.warn({ error, eventType: parsedEvent.data.type }, "\u{1F4D6} Error in EventBus typed subscriber");
3232
+ }
3225
3233
  return true;
3226
3234
  }
3227
3235
  /**
@@ -3487,6 +3495,7 @@ var WSServer = class {
3487
3495
  });
3488
3496
  socket.on("error", (error) => {
3489
3497
  logger.warn({ error }, "WebSocket consumer error");
3498
+ this.consumers.delete(socket);
3490
3499
  });
3491
3500
  const welcomeMessage = {
3492
3501
  type: "welcome",
@@ -3643,10 +3652,17 @@ var HTTPReceiver = class {
3643
3652
  }
3644
3653
  async handleRequest(request, response, options) {
3645
3654
  this.requestCount += 1;
3646
- const requestUrl = new URL(
3647
- request.url ?? "/",
3648
- `http://${this.host}:${this.port ?? options.port}`
3649
- );
3655
+ let requestUrl;
3656
+ try {
3657
+ requestUrl = new URL(
3658
+ request.url ?? "/",
3659
+ `http://${this.host}:${this.port ?? options.port}`
3660
+ );
3661
+ } catch {
3662
+ this.invalidRequestCount += 1;
3663
+ this.sendJson(response, 400, { error: "malformed request url" });
3664
+ return;
3665
+ }
3650
3666
  if (request.method === "GET" && requestUrl.pathname === "/health") {
3651
3667
  this.sendJson(response, 200, options.getHealthSnapshot());
3652
3668
  return;
@@ -3987,15 +4003,24 @@ var BaseAdapter = class {
3987
4003
  cwd: data.cwd ?? context.cwd
3988
4004
  }
3989
4005
  });
3990
- const published = await this.publishEventImplementation(event, {
3991
- cwd: context.cwd,
3992
- env: context.env,
3993
- hookPayload: context.hookPayload,
3994
- pid: context.pid,
3995
- sessionId,
3996
- source: context.source,
3997
- transcriptPath: context.transcriptPath
3998
- });
4006
+ let published;
4007
+ try {
4008
+ published = await this.publishEventImplementation(event, {
4009
+ cwd: context.cwd,
4010
+ env: context.env,
4011
+ hookPayload: context.hookPayload,
4012
+ pid: context.pid,
4013
+ sessionId,
4014
+ source: context.source,
4015
+ transcriptPath: context.transcriptPath
4016
+ });
4017
+ } catch (error) {
4018
+ logger.error(
4019
+ { error, eventType: type, adapter: this.name, sessionId },
4020
+ "\u{1F4D6} Failed to publish event \u2014 swallowing to prevent daemon crash"
4021
+ );
4022
+ published = false;
4023
+ }
3999
4024
  if (published) {
4000
4025
  this.eventsEmitted += 1;
4001
4026
  }
@@ -9123,22 +9148,40 @@ var AdapterRegistry = class {
9123
9148
  }
9124
9149
  /**
9125
9150
  * Starts every adapter enabled in the current AISnitch config.
9151
+ * 📖 Each adapter is started independently — one failure does not prevent
9152
+ * the others from starting.
9126
9153
  */
9127
9154
  async startAll(config) {
9128
9155
  for (const adapter of this.list()) {
9129
9156
  if (config.adapters[adapter.name]?.enabled !== true) {
9130
9157
  continue;
9131
9158
  }
9132
- await adapter.start();
9159
+ try {
9160
+ await adapter.start();
9161
+ } catch (error) {
9162
+ logger.error(
9163
+ { error, adapter: adapter.name },
9164
+ `\u{1F4D6} Failed to start adapter "${adapter.name}" \u2014 skipping`
9165
+ );
9166
+ }
9133
9167
  }
9134
9168
  }
9135
9169
  /**
9136
9170
  * Stops every adapter in reverse registration order.
9171
+ * 📖 Each adapter is stopped independently — one failure does not prevent
9172
+ * the others from being stopped.
9137
9173
  */
9138
9174
  async stopAll() {
9139
9175
  const adapters = this.list().reverse();
9140
9176
  for (const adapter of adapters) {
9141
- await adapter.stop();
9177
+ try {
9178
+ await adapter.stop();
9179
+ } catch (error) {
9180
+ logger.warn(
9181
+ { error, adapter: adapter.name },
9182
+ `\u{1F4D6} Error stopping adapter "${adapter.name}" \u2014 continuing`
9183
+ );
9184
+ }
9142
9185
  }
9143
9186
  }
9144
9187
  };
@@ -9243,26 +9286,32 @@ var Pipeline = class {
9243
9286
  await adapter.handleHook(payload);
9244
9287
  });
9245
9288
  }
9289
+ try {
9290
+ this.wsPort = await this.wsServer.start({
9291
+ port: resolvedWsPort,
9292
+ eventBus: this.eventBus,
9293
+ activeTools
9294
+ });
9295
+ this.httpPort = await this.httpReceiver.start({
9296
+ port: resolvedHttpPort,
9297
+ onHook: async (tool, payload) => {
9298
+ await this.handleHook(tool, payload);
9299
+ },
9300
+ getHealthSnapshot: () => this.getHealthSnapshot()
9301
+ });
9302
+ this.socketPath = await this.udsServer.start({
9303
+ socketPath,
9304
+ onEvent: async (event) => {
9305
+ await this.publishEvent(event);
9306
+ }
9307
+ });
9308
+ await this.adapterRegistry.startAll(config);
9309
+ } catch (error) {
9310
+ logger.error({ error }, "\u{1F4D6} Pipeline start failed \u2014 rolling back already-started components");
9311
+ await this.rollbackPartialStart();
9312
+ throw error;
9313
+ }
9246
9314
  this.startedAt = Date.now();
9247
- this.wsPort = await this.wsServer.start({
9248
- port: resolvedWsPort,
9249
- eventBus: this.eventBus,
9250
- activeTools
9251
- });
9252
- this.httpPort = await this.httpReceiver.start({
9253
- port: resolvedHttpPort,
9254
- onHook: async (tool, payload) => {
9255
- await this.handleHook(tool, payload);
9256
- },
9257
- getHealthSnapshot: () => this.getHealthSnapshot()
9258
- });
9259
- this.socketPath = await this.udsServer.start({
9260
- socketPath,
9261
- onEvent: async (event) => {
9262
- await this.publishEvent(event);
9263
- }
9264
- });
9265
- await this.adapterRegistry.startAll(config);
9266
9315
  logger.info(this.getStatus(), "Core pipeline started");
9267
9316
  return this.getStatus();
9268
9317
  }
@@ -9270,10 +9319,25 @@ var Pipeline = class {
9270
9319
  * Stops every pipeline component in reverse dependency order.
9271
9320
  */
9272
9321
  async stop() {
9273
- await this.adapterRegistry?.stopAll();
9274
- await this.httpReceiver.stop();
9275
- await this.udsServer.stop();
9276
- await this.wsServer.stop();
9322
+ const stopSafely = async (label, fn) => {
9323
+ try {
9324
+ await fn();
9325
+ } catch (error) {
9326
+ logger.warn({ error }, `\u{1F4D6} Error while stopping ${label} \u2014 continuing shutdown`);
9327
+ }
9328
+ };
9329
+ await stopSafely("adapter registry", async () => {
9330
+ await this.adapterRegistry?.stopAll();
9331
+ });
9332
+ await stopSafely("HTTP receiver", async () => {
9333
+ await this.httpReceiver.stop();
9334
+ });
9335
+ await stopSafely("UDS server", async () => {
9336
+ await this.udsServer.stop();
9337
+ });
9338
+ await stopSafely("WebSocket server", async () => {
9339
+ await this.wsServer.stop();
9340
+ });
9277
9341
  this.eventBus.unsubscribeAll();
9278
9342
  this.adapterRegistry = null;
9279
9343
  this.enabledTools.clear();
@@ -9290,11 +9354,50 @@ var Pipeline = class {
9290
9354
  registerHookHandler(tool, handler) {
9291
9355
  this.hookHandlers.set(tool, handler);
9292
9356
  }
9357
+ /**
9358
+ * 📖 Rolls back any components that were successfully started before a
9359
+ * failure occurred, preventing orphaned servers or leaking resources.
9360
+ */
9361
+ async rollbackPartialStart() {
9362
+ const stopSafe = async (label, fn) => {
9363
+ try {
9364
+ await fn();
9365
+ } catch (error) {
9366
+ logger.warn({ error }, `\u{1F4D6} Error rolling back ${label}`);
9367
+ }
9368
+ };
9369
+ await stopSafe("adapter registry", async () => {
9370
+ await this.adapterRegistry?.stopAll();
9371
+ });
9372
+ await stopSafe("UDS server", async () => {
9373
+ await this.udsServer.stop();
9374
+ });
9375
+ await stopSafe("HTTP receiver", async () => {
9376
+ await this.httpReceiver.stop();
9377
+ });
9378
+ await stopSafe("WebSocket server", async () => {
9379
+ await this.wsServer.stop();
9380
+ });
9381
+ this.adapterRegistry = null;
9382
+ this.enabledTools.clear();
9383
+ this.hookHandlers.clear();
9384
+ }
9293
9385
  /**
9294
9386
  * Publishes an event after best-effort context enrichment.
9387
+ * 📖 If enrichment fails, the original event is published un-enriched
9388
+ * rather than being dropped entirely.
9295
9389
  */
9296
9390
  async publishEvent(event, context = {}) {
9297
- const enrichedEvent = await this.contextDetector.enrich(event, context);
9391
+ let enrichedEvent;
9392
+ try {
9393
+ enrichedEvent = await this.contextDetector.enrich(event, context);
9394
+ } catch (error) {
9395
+ logger.warn(
9396
+ { error, eventId: event.id },
9397
+ "\u{1F4D6} Context enrichment failed \u2014 publishing un-enriched event"
9398
+ );
9399
+ enrichedEvent = event;
9400
+ }
9298
9401
  return this.eventBus.publish(enrichedEvent);
9299
9402
  }
9300
9403
  /**
@@ -9330,6 +9433,16 @@ var Pipeline = class {
9330
9433
  };
9331
9434
  }
9332
9435
  async handleHook(tool, payload) {
9436
+ try {
9437
+ await this.handleHookInner(tool, payload);
9438
+ } catch (error) {
9439
+ logger.error(
9440
+ { error, tool },
9441
+ "\u{1F4D6} Unhandled error in hook handler \u2014 swallowing to prevent daemon crash"
9442
+ );
9443
+ }
9444
+ }
9445
+ async handleHookInner(tool, payload) {
9333
9446
  if (!this.enabledTools.has(tool)) {
9334
9447
  logger.debug({ tool }, "Ignoring hook for disabled tool");
9335
9448
  return;
@@ -10505,18 +10618,22 @@ function parseSocketPayload(data) {
10505
10618
  return parsedEvent.success ? parsedEvent.data : null;
10506
10619
  }
10507
10620
  function parseUnknownPayload(data) {
10508
- if (typeof data === "string") {
10509
- return JSON.parse(data);
10510
- }
10511
- if (Array.isArray(data)) {
10512
- return JSON.parse(Buffer.concat(data).toString("utf8"));
10513
- }
10514
- if (data instanceof ArrayBuffer) {
10515
- return JSON.parse(
10516
- Buffer.from(new Uint8Array(data)).toString("utf8")
10517
- );
10621
+ try {
10622
+ if (typeof data === "string") {
10623
+ return JSON.parse(data);
10624
+ }
10625
+ if (Array.isArray(data)) {
10626
+ return JSON.parse(Buffer.concat(data).toString("utf8"));
10627
+ }
10628
+ if (data instanceof ArrayBuffer) {
10629
+ return JSON.parse(
10630
+ Buffer.from(new Uint8Array(data)).toString("utf8")
10631
+ );
10632
+ }
10633
+ return JSON.parse(Buffer.from(data).toString("utf8"));
10634
+ } catch {
10635
+ return null;
10518
10636
  }
10519
- return JSON.parse(Buffer.from(data).toString("utf8"));
10520
10637
  }
10521
10638
 
10522
10639
  // src/tui/hooks/useKeyBinds.ts
@@ -11244,16 +11361,20 @@ function ManagedDaemonApp({
11244
11361
  }
11245
11362
  function parseSocketPayload2(data) {
11246
11363
  let parsedPayload;
11247
- if (typeof data === "string") {
11248
- parsedPayload = JSON.parse(data);
11249
- } else if (Array.isArray(data)) {
11250
- parsedPayload = JSON.parse(Buffer.concat(data).toString("utf8"));
11251
- } else if (data instanceof ArrayBuffer) {
11252
- parsedPayload = JSON.parse(
11253
- Buffer.from(new Uint8Array(data)).toString("utf8")
11254
- );
11255
- } else {
11256
- parsedPayload = JSON.parse(Buffer.from(data).toString("utf8"));
11364
+ try {
11365
+ if (typeof data === "string") {
11366
+ parsedPayload = JSON.parse(data);
11367
+ } else if (Array.isArray(data)) {
11368
+ parsedPayload = JSON.parse(Buffer.concat(data).toString("utf8"));
11369
+ } else if (data instanceof ArrayBuffer) {
11370
+ parsedPayload = JSON.parse(
11371
+ Buffer.from(new Uint8Array(data)).toString("utf8")
11372
+ );
11373
+ } else {
11374
+ parsedPayload = JSON.parse(Buffer.from(data).toString("utf8"));
11375
+ }
11376
+ } catch {
11377
+ return null;
11257
11378
  }
11258
11379
  if (typeof parsedPayload === "object" && parsedPayload !== null && "type" in parsedPayload && parsedPayload.type === "welcome") {
11259
11380
  return null;
@@ -11900,16 +12021,20 @@ function hexToRgb(hexColor) {
11900
12021
  ];
11901
12022
  }
11902
12023
  function parseSocketMessage(data) {
11903
- if (typeof data === "string") {
11904
- return JSON.parse(data);
11905
- }
11906
- if (Array.isArray(data)) {
11907
- return JSON.parse(Buffer.concat(data).toString("utf8"));
11908
- }
11909
- if (data instanceof ArrayBuffer) {
11910
- return JSON.parse(Buffer.from(new Uint8Array(data)).toString("utf8"));
12024
+ try {
12025
+ if (typeof data === "string") {
12026
+ return JSON.parse(data);
12027
+ }
12028
+ if (Array.isArray(data)) {
12029
+ return JSON.parse(Buffer.concat(data).toString("utf8"));
12030
+ }
12031
+ if (data instanceof ArrayBuffer) {
12032
+ return JSON.parse(Buffer.from(new Uint8Array(data)).toString("utf8"));
12033
+ }
12034
+ return JSON.parse(Buffer.from(data).toString("utf8"));
12035
+ } catch {
12036
+ return null;
11911
12037
  }
11912
- return JSON.parse(Buffer.from(data).toString("utf8"));
11913
12038
  }
11914
12039
  function isWelcomeMessage(payload) {
11915
12040
  if (!isRecord10(payload)) {
@@ -12257,6 +12382,20 @@ function createCliRuntime(dependencies = {}) {
12257
12382
  process.once("SIGINT", () => {
12258
12383
  void shutdown("SIGINT");
12259
12384
  });
12385
+ process.once("uncaughtException", (error) => {
12386
+ output.stderr(
12387
+ `AISnitch crashed: ${error instanceof Error ? error.message : "unknown exception"}
12388
+ `
12389
+ );
12390
+ void shutdown("uncaughtException", 1);
12391
+ });
12392
+ process.once("unhandledRejection", (reason) => {
12393
+ output.stderr(
12394
+ `AISnitch rejected a promise: ${reason instanceof Error ? reason.message : "unknown rejection"}
12395
+ `
12396
+ );
12397
+ void shutdown("unhandledRejection", 1);
12398
+ });
12260
12399
  await renderForegroundTui({
12261
12400
  configuredAdapters: getEnabledAdapters(config),
12262
12401
  eventBus: pipeline.getEventBus(),