aisnitch 0.2.3 → 0.2.5

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/index.d.cts CHANGED
@@ -81,10 +81,10 @@ declare const ConfigSchema: z.ZodObject<{
81
81
  }, z.core.$strict>>;
82
82
  idleTimeoutMs: z.ZodDefault<z.ZodNumber>;
83
83
  logLevel: z.ZodDefault<z.ZodEnum<{
84
- error: "error";
85
84
  debug: "debug";
86
85
  info: "info";
87
86
  warn: "warn";
87
+ error: "error";
88
88
  }>>;
89
89
  }, z.core.$strict>;
90
90
  /**
@@ -948,10 +948,14 @@ declare class AdapterRegistry {
948
948
  getStatus(): AdapterStatus[];
949
949
  /**
950
950
  * Starts every adapter enabled in the current AISnitch config.
951
+ * 📖 Each adapter is started independently — one failure does not prevent
952
+ * the others from starting.
951
953
  */
952
954
  startAll(config: AISnitchConfig): Promise<void>;
953
955
  /**
954
956
  * Stops every adapter in reverse registration order.
957
+ * 📖 Each adapter is stopped independently — one failure does not prevent
958
+ * the others from being stopped.
955
959
  */
956
960
  stopAll(): Promise<void>;
957
961
  }
@@ -1615,8 +1619,15 @@ declare class Pipeline {
1615
1619
  * Registers a future adapter-specific hook handler for one tool.
1616
1620
  */
1617
1621
  registerHookHandler(tool: ToolName, handler: HookHandler): void;
1622
+ /**
1623
+ * 📖 Rolls back any components that were successfully started before a
1624
+ * failure occurred, preventing orphaned servers or leaking resources.
1625
+ */
1626
+ private rollbackPartialStart;
1618
1627
  /**
1619
1628
  * Publishes an event after best-effort context enrichment.
1629
+ * 📖 If enrichment fails, the original event is published un-enriched
1630
+ * rather than being dropped entirely.
1620
1631
  */
1621
1632
  publishEvent(event: AISnitchEvent, context?: ProcessContext): Promise<boolean>;
1622
1633
  /**
@@ -1629,6 +1640,7 @@ declare class Pipeline {
1629
1640
  getEventBus(): EventBus;
1630
1641
  private getHealthSnapshot;
1631
1642
  private handleHook;
1643
+ private handleHookInner;
1632
1644
  private isPlainRecord;
1633
1645
  }
1634
1646
 
package/dist/index.d.ts CHANGED
@@ -81,10 +81,10 @@ declare const ConfigSchema: z.ZodObject<{
81
81
  }, z.core.$strict>>;
82
82
  idleTimeoutMs: z.ZodDefault<z.ZodNumber>;
83
83
  logLevel: z.ZodDefault<z.ZodEnum<{
84
- error: "error";
85
84
  debug: "debug";
86
85
  info: "info";
87
86
  warn: "warn";
87
+ error: "error";
88
88
  }>>;
89
89
  }, z.core.$strict>;
90
90
  /**
@@ -948,10 +948,14 @@ declare class AdapterRegistry {
948
948
  getStatus(): AdapterStatus[];
949
949
  /**
950
950
  * Starts every adapter enabled in the current AISnitch config.
951
+ * 📖 Each adapter is started independently — one failure does not prevent
952
+ * the others from starting.
951
953
  */
952
954
  startAll(config: AISnitchConfig): Promise<void>;
953
955
  /**
954
956
  * Stops every adapter in reverse registration order.
957
+ * 📖 Each adapter is stopped independently — one failure does not prevent
958
+ * the others from being stopped.
955
959
  */
956
960
  stopAll(): Promise<void>;
957
961
  }
@@ -1615,8 +1619,15 @@ declare class Pipeline {
1615
1619
  * Registers a future adapter-specific hook handler for one tool.
1616
1620
  */
1617
1621
  registerHookHandler(tool: ToolName, handler: HookHandler): void;
1622
+ /**
1623
+ * 📖 Rolls back any components that were successfully started before a
1624
+ * failure occurred, preventing orphaned servers or leaking resources.
1625
+ */
1626
+ private rollbackPartialStart;
1618
1627
  /**
1619
1628
  * Publishes an event after best-effort context enrichment.
1629
+ * 📖 If enrichment fails, the original event is published un-enriched
1630
+ * rather than being dropped entirely.
1620
1631
  */
1621
1632
  publishEvent(event: AISnitchEvent, context?: ProcessContext): Promise<boolean>;
1622
1633
  /**
@@ -1629,6 +1640,7 @@ declare class Pipeline {
1629
1640
  getEventBus(): EventBus;
1630
1641
  private getHealthSnapshot;
1631
1642
  private handleHook;
1643
+ private handleHookInner;
1632
1644
  private isPlainRecord;
1633
1645
  }
1634
1646
 
package/dist/index.js CHANGED
@@ -346,15 +346,24 @@ var BaseAdapter = class {
346
346
  cwd: data.cwd ?? context.cwd
347
347
  }
348
348
  });
349
- const published = await this.publishEventImplementation(event, {
350
- cwd: context.cwd,
351
- env: context.env,
352
- hookPayload: context.hookPayload,
353
- pid: context.pid,
354
- sessionId,
355
- source: context.source,
356
- transcriptPath: context.transcriptPath
357
- });
349
+ let published;
350
+ try {
351
+ published = await this.publishEventImplementation(event, {
352
+ cwd: context.cwd,
353
+ env: context.env,
354
+ hookPayload: context.hookPayload,
355
+ pid: context.pid,
356
+ sessionId,
357
+ source: context.source,
358
+ transcriptPath: context.transcriptPath
359
+ });
360
+ } catch (error) {
361
+ logger.error(
362
+ { error, eventType: type, adapter: this.name, sessionId },
363
+ "\u{1F4D6} Failed to publish event \u2014 swallowing to prevent daemon crash"
364
+ );
365
+ published = false;
366
+ }
358
367
  if (published) {
359
368
  this.eventsEmitted += 1;
360
369
  }
@@ -1340,6 +1349,9 @@ var ClaudeCodeAdapter = class extends BaseAdapter {
1340
1349
  });
1341
1350
  const context = {
1342
1351
  cwd: getString(payload, "cwd"),
1352
+ // 📖 Pass process.env so the context detector can detect the terminal
1353
+ // from TERM_PROGRAM, ITERM_SESSION_ID, etc. — hooks don't carry env vars
1354
+ env: this.env ?? process.env,
1343
1355
  hookPayload: payload,
1344
1356
  pid: getNumber(payload, "pid"),
1345
1357
  sessionId,
@@ -1573,6 +1585,8 @@ function extractClaudeTranscriptObservations(payload, transcriptPath) {
1573
1585
  const tokensUsed = extractTokenUsage(payload);
1574
1586
  const rawPayload = payload;
1575
1587
  const sharedContext = {
1588
+ // 📖 Pass process.env so terminal detection works from transcript path too
1589
+ env: process.env,
1576
1590
  hookPayload: rawPayload,
1577
1591
  sessionId,
1578
1592
  source: "aisnitch://adapters/claude-code/transcript",
@@ -5229,6 +5243,8 @@ var OpenCodeAdapter = class extends BaseAdapter {
5229
5243
  });
5230
5244
  const context = {
5231
5245
  cwd: extractOpenCodeCwd(payload),
5246
+ // 📖 Pass process.env so the context detector can detect the terminal
5247
+ env: this.env ?? process.env,
5232
5248
  hookPayload: payload,
5233
5249
  pid: getNumber5(payload, "pid"),
5234
5250
  sessionId,
@@ -5239,6 +5255,8 @@ var OpenCodeAdapter = class extends BaseAdapter {
5239
5255
  cwd: context.cwd,
5240
5256
  errorMessage: extractOpenCodeErrorMessage(payload),
5241
5257
  errorType: extractOpenCodeErrorType(payload),
5258
+ // 📖 Extract model from payload — OpenCode may send it as "model" or nested in properties
5259
+ model: getString7(payload, "model") ?? getString7(getRecord6(payload.properties), "model"),
5242
5260
  project: extractOpenCodeProject(payload),
5243
5261
  raw: payload,
5244
5262
  toolInput: extractOpenCodeToolInput(payload),
@@ -5482,22 +5500,40 @@ var AdapterRegistry = class {
5482
5500
  }
5483
5501
  /**
5484
5502
  * Starts every adapter enabled in the current AISnitch config.
5503
+ * 📖 Each adapter is started independently — one failure does not prevent
5504
+ * the others from starting.
5485
5505
  */
5486
5506
  async startAll(config) {
5487
5507
  for (const adapter of this.list()) {
5488
5508
  if (config.adapters[adapter.name]?.enabled !== true) {
5489
5509
  continue;
5490
5510
  }
5491
- await adapter.start();
5511
+ try {
5512
+ await adapter.start();
5513
+ } catch (error) {
5514
+ logger.error(
5515
+ { error, adapter: adapter.name },
5516
+ `\u{1F4D6} Failed to start adapter "${adapter.name}" \u2014 skipping`
5517
+ );
5518
+ }
5492
5519
  }
5493
5520
  }
5494
5521
  /**
5495
5522
  * Stops every adapter in reverse registration order.
5523
+ * 📖 Each adapter is stopped independently — one failure does not prevent
5524
+ * the others from being stopped.
5496
5525
  */
5497
5526
  async stopAll() {
5498
5527
  const adapters = this.list().reverse();
5499
5528
  for (const adapter of adapters) {
5500
- await adapter.stop();
5529
+ try {
5530
+ await adapter.stop();
5531
+ } catch (error) {
5532
+ logger.warn(
5533
+ { error, adapter: adapter.name },
5534
+ `\u{1F4D6} Error stopping adapter "${adapter.name}" \u2014 continuing`
5535
+ );
5536
+ }
5501
5537
  }
5502
5538
  }
5503
5539
  };
@@ -6460,8 +6496,16 @@ var EventBus = class {
6460
6496
  },
6461
6497
  "Published event"
6462
6498
  );
6463
- this.emitter.emit("event", parsedEvent.data);
6464
- this.emitter.emit(`event:${parsedEvent.data.type}`, parsedEvent.data);
6499
+ try {
6500
+ this.emitter.emit("event", parsedEvent.data);
6501
+ } catch (error) {
6502
+ logger.warn({ error, eventType: parsedEvent.data.type }, "\u{1F4D6} Error in EventBus global subscriber");
6503
+ }
6504
+ try {
6505
+ this.emitter.emit(`event:${parsedEvent.data.type}`, parsedEvent.data);
6506
+ } catch (error) {
6507
+ logger.warn({ error, eventType: parsedEvent.data.type }, "\u{1F4D6} Error in EventBus typed subscriber");
6508
+ }
6465
6509
  return true;
6466
6510
  }
6467
6511
  /**
@@ -6742,6 +6786,7 @@ var WSServer = class {
6742
6786
  });
6743
6787
  socket.on("error", (error) => {
6744
6788
  logger.warn({ error }, "WebSocket consumer error");
6789
+ this.consumers.delete(socket);
6745
6790
  });
6746
6791
  const welcomeMessage = {
6747
6792
  type: "welcome",
@@ -6898,10 +6943,17 @@ var HTTPReceiver = class {
6898
6943
  }
6899
6944
  async handleRequest(request, response, options) {
6900
6945
  this.requestCount += 1;
6901
- const requestUrl = new URL(
6902
- request.url ?? "/",
6903
- `http://${this.host}:${this.port ?? options.port}`
6904
- );
6946
+ let requestUrl;
6947
+ try {
6948
+ requestUrl = new URL(
6949
+ request.url ?? "/",
6950
+ `http://${this.host}:${this.port ?? options.port}`
6951
+ );
6952
+ } catch {
6953
+ this.invalidRequestCount += 1;
6954
+ this.sendJson(response, 400, { error: "malformed request url" });
6955
+ return;
6956
+ }
6905
6957
  if (request.method === "GET" && requestUrl.pathname === "/health") {
6906
6958
  this.sendJson(response, 200, options.getHealthSnapshot());
6907
6959
  return;
@@ -7246,26 +7298,32 @@ var Pipeline = class {
7246
7298
  await adapter.handleHook(payload);
7247
7299
  });
7248
7300
  }
7301
+ try {
7302
+ this.wsPort = await this.wsServer.start({
7303
+ port: resolvedWsPort,
7304
+ eventBus: this.eventBus,
7305
+ activeTools
7306
+ });
7307
+ this.httpPort = await this.httpReceiver.start({
7308
+ port: resolvedHttpPort,
7309
+ onHook: async (tool, payload) => {
7310
+ await this.handleHook(tool, payload);
7311
+ },
7312
+ getHealthSnapshot: () => this.getHealthSnapshot()
7313
+ });
7314
+ this.socketPath = await this.udsServer.start({
7315
+ socketPath,
7316
+ onEvent: async (event) => {
7317
+ await this.publishEvent(event);
7318
+ }
7319
+ });
7320
+ await this.adapterRegistry.startAll(config);
7321
+ } catch (error) {
7322
+ logger.error({ error }, "\u{1F4D6} Pipeline start failed \u2014 rolling back already-started components");
7323
+ await this.rollbackPartialStart();
7324
+ throw error;
7325
+ }
7249
7326
  this.startedAt = Date.now();
7250
- this.wsPort = await this.wsServer.start({
7251
- port: resolvedWsPort,
7252
- eventBus: this.eventBus,
7253
- activeTools
7254
- });
7255
- this.httpPort = await this.httpReceiver.start({
7256
- port: resolvedHttpPort,
7257
- onHook: async (tool, payload) => {
7258
- await this.handleHook(tool, payload);
7259
- },
7260
- getHealthSnapshot: () => this.getHealthSnapshot()
7261
- });
7262
- this.socketPath = await this.udsServer.start({
7263
- socketPath,
7264
- onEvent: async (event) => {
7265
- await this.publishEvent(event);
7266
- }
7267
- });
7268
- await this.adapterRegistry.startAll(config);
7269
7327
  logger.info(this.getStatus(), "Core pipeline started");
7270
7328
  return this.getStatus();
7271
7329
  }
@@ -7273,10 +7331,25 @@ var Pipeline = class {
7273
7331
  * Stops every pipeline component in reverse dependency order.
7274
7332
  */
7275
7333
  async stop() {
7276
- await this.adapterRegistry?.stopAll();
7277
- await this.httpReceiver.stop();
7278
- await this.udsServer.stop();
7279
- await this.wsServer.stop();
7334
+ const stopSafely = async (label, fn) => {
7335
+ try {
7336
+ await fn();
7337
+ } catch (error) {
7338
+ logger.warn({ error }, `\u{1F4D6} Error while stopping ${label} \u2014 continuing shutdown`);
7339
+ }
7340
+ };
7341
+ await stopSafely("adapter registry", async () => {
7342
+ await this.adapterRegistry?.stopAll();
7343
+ });
7344
+ await stopSafely("HTTP receiver", async () => {
7345
+ await this.httpReceiver.stop();
7346
+ });
7347
+ await stopSafely("UDS server", async () => {
7348
+ await this.udsServer.stop();
7349
+ });
7350
+ await stopSafely("WebSocket server", async () => {
7351
+ await this.wsServer.stop();
7352
+ });
7280
7353
  this.eventBus.unsubscribeAll();
7281
7354
  this.adapterRegistry = null;
7282
7355
  this.enabledTools.clear();
@@ -7293,11 +7366,50 @@ var Pipeline = class {
7293
7366
  registerHookHandler(tool, handler) {
7294
7367
  this.hookHandlers.set(tool, handler);
7295
7368
  }
7369
+ /**
7370
+ * 📖 Rolls back any components that were successfully started before a
7371
+ * failure occurred, preventing orphaned servers or leaking resources.
7372
+ */
7373
+ async rollbackPartialStart() {
7374
+ const stopSafe = async (label, fn) => {
7375
+ try {
7376
+ await fn();
7377
+ } catch (error) {
7378
+ logger.warn({ error }, `\u{1F4D6} Error rolling back ${label}`);
7379
+ }
7380
+ };
7381
+ await stopSafe("adapter registry", async () => {
7382
+ await this.adapterRegistry?.stopAll();
7383
+ });
7384
+ await stopSafe("UDS server", async () => {
7385
+ await this.udsServer.stop();
7386
+ });
7387
+ await stopSafe("HTTP receiver", async () => {
7388
+ await this.httpReceiver.stop();
7389
+ });
7390
+ await stopSafe("WebSocket server", async () => {
7391
+ await this.wsServer.stop();
7392
+ });
7393
+ this.adapterRegistry = null;
7394
+ this.enabledTools.clear();
7395
+ this.hookHandlers.clear();
7396
+ }
7296
7397
  /**
7297
7398
  * Publishes an event after best-effort context enrichment.
7399
+ * 📖 If enrichment fails, the original event is published un-enriched
7400
+ * rather than being dropped entirely.
7298
7401
  */
7299
7402
  async publishEvent(event, context = {}) {
7300
- const enrichedEvent = await this.contextDetector.enrich(event, context);
7403
+ let enrichedEvent;
7404
+ try {
7405
+ enrichedEvent = await this.contextDetector.enrich(event, context);
7406
+ } catch (error) {
7407
+ logger.warn(
7408
+ { error, eventId: event.id },
7409
+ "\u{1F4D6} Context enrichment failed \u2014 publishing un-enriched event"
7410
+ );
7411
+ enrichedEvent = event;
7412
+ }
7301
7413
  return this.eventBus.publish(enrichedEvent);
7302
7414
  }
7303
7415
  /**
@@ -7333,6 +7445,16 @@ var Pipeline = class {
7333
7445
  };
7334
7446
  }
7335
7447
  async handleHook(tool, payload) {
7448
+ try {
7449
+ await this.handleHookInner(tool, payload);
7450
+ } catch (error) {
7451
+ logger.error(
7452
+ { error, tool },
7453
+ "\u{1F4D6} Unhandled error in hook handler \u2014 swallowing to prevent daemon crash"
7454
+ );
7455
+ }
7456
+ }
7457
+ async handleHookInner(tool, payload) {
7336
7458
  if (!this.enabledTools.has(tool)) {
7337
7459
  logger.debug({ tool }, "Ignoring hook for disabled tool");
7338
7460
  return;
@@ -8508,18 +8630,22 @@ function parseSocketPayload(data) {
8508
8630
  return parsedEvent.success ? parsedEvent.data : null;
8509
8631
  }
8510
8632
  function parseUnknownPayload(data) {
8511
- if (typeof data === "string") {
8512
- return JSON.parse(data);
8513
- }
8514
- if (Array.isArray(data)) {
8515
- return JSON.parse(Buffer.concat(data).toString("utf8"));
8516
- }
8517
- if (data instanceof ArrayBuffer) {
8518
- return JSON.parse(
8519
- Buffer.from(new Uint8Array(data)).toString("utf8")
8520
- );
8633
+ try {
8634
+ if (typeof data === "string") {
8635
+ return JSON.parse(data);
8636
+ }
8637
+ if (Array.isArray(data)) {
8638
+ return JSON.parse(Buffer.concat(data).toString("utf8"));
8639
+ }
8640
+ if (data instanceof ArrayBuffer) {
8641
+ return JSON.parse(
8642
+ Buffer.from(new Uint8Array(data)).toString("utf8")
8643
+ );
8644
+ }
8645
+ return JSON.parse(Buffer.from(data).toString("utf8"));
8646
+ } catch {
8647
+ return null;
8521
8648
  }
8522
- return JSON.parse(Buffer.from(data).toString("utf8"));
8523
8649
  }
8524
8650
 
8525
8651
  // src/tui/hooks/useKeyBinds.ts
@@ -9247,16 +9373,20 @@ function ManagedDaemonApp({
9247
9373
  }
9248
9374
  function parseSocketPayload2(data) {
9249
9375
  let parsedPayload;
9250
- if (typeof data === "string") {
9251
- parsedPayload = JSON.parse(data);
9252
- } else if (Array.isArray(data)) {
9253
- parsedPayload = JSON.parse(Buffer.concat(data).toString("utf8"));
9254
- } else if (data instanceof ArrayBuffer) {
9255
- parsedPayload = JSON.parse(
9256
- Buffer.from(new Uint8Array(data)).toString("utf8")
9257
- );
9258
- } else {
9259
- parsedPayload = JSON.parse(Buffer.from(data).toString("utf8"));
9376
+ try {
9377
+ if (typeof data === "string") {
9378
+ parsedPayload = JSON.parse(data);
9379
+ } else if (Array.isArray(data)) {
9380
+ parsedPayload = JSON.parse(Buffer.concat(data).toString("utf8"));
9381
+ } else if (data instanceof ArrayBuffer) {
9382
+ parsedPayload = JSON.parse(
9383
+ Buffer.from(new Uint8Array(data)).toString("utf8")
9384
+ );
9385
+ } else {
9386
+ parsedPayload = JSON.parse(Buffer.from(data).toString("utf8"));
9387
+ }
9388
+ } catch {
9389
+ return null;
9260
9390
  }
9261
9391
  if (typeof parsedPayload === "object" && parsedPayload !== null && "type" in parsedPayload && parsedPayload.type === "welcome") {
9262
9392
  return null;
@@ -9327,18 +9457,22 @@ async function attachWebSocketMonitor(url, output) {
9327
9457
  };
9328
9458
  }
9329
9459
  function parseSocketMessage(data) {
9330
- if (typeof data === "string") {
9331
- return JSON.parse(data);
9332
- }
9333
- if (Array.isArray(data)) {
9334
- return JSON.parse(Buffer.concat(data).toString("utf8"));
9335
- }
9336
- if (data instanceof ArrayBuffer) {
9337
- return JSON.parse(
9338
- Buffer.from(new Uint8Array(data)).toString("utf8")
9339
- );
9460
+ try {
9461
+ if (typeof data === "string") {
9462
+ return JSON.parse(data);
9463
+ }
9464
+ if (Array.isArray(data)) {
9465
+ return JSON.parse(Buffer.concat(data).toString("utf8"));
9466
+ }
9467
+ if (data instanceof ArrayBuffer) {
9468
+ return JSON.parse(
9469
+ Buffer.from(new Uint8Array(data)).toString("utf8")
9470
+ );
9471
+ }
9472
+ return JSON.parse(Buffer.from(data).toString("utf8"));
9473
+ } catch {
9474
+ return null;
9340
9475
  }
9341
- return JSON.parse(Buffer.from(data).toString("utf8"));
9342
9476
  }
9343
9477
  function isWelcomeMessage(payload) {
9344
9478
  if (typeof payload !== "object" || payload === null) {