aisnitch 0.2.18 → 0.2.20

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.
@@ -41,7 +41,7 @@ var import_commander = require("commander");
41
41
 
42
42
  // src/package-info.ts
43
43
  var AISNITCH_PACKAGE_NAME = "aisnitch";
44
- var AISNITCH_VERSION = "0.2.18";
44
+ var AISNITCH_VERSION = "0.2.20";
45
45
  var AISNITCH_DESCRIPTION = "Universal bridge for AI coding tool activity \u2014 capture, normalize, stream.";
46
46
 
47
47
  // src/core/events/schema.ts
@@ -123,8 +123,8 @@ function createUuidV7() {
123
123
  return (0, import_uuid.v7)();
124
124
  }
125
125
  var ToolInputSchema = import_zod.z.strictObject({
126
- filePath: import_zod.z.string().min(1).optional(),
127
- command: import_zod.z.string().min(1).optional()
126
+ filePath: import_zod.z.string().min(1).max(4096).optional(),
127
+ command: import_zod.z.string().min(1).max(1e4).optional()
128
128
  }).refine(
129
129
  (value) => value.filePath !== void 0 || value.command !== void 0,
130
130
  "toolInput must include filePath or command"
@@ -135,35 +135,35 @@ var ErrorTypeSchema = import_zod.z.enum(ERROR_TYPES);
135
135
  var CESPCategorySchema = import_zod.z.enum(CESP_CATEGORIES);
136
136
  var EventDataSchema = import_zod.z.strictObject({
137
137
  state: AISnitchEventTypeSchema,
138
- project: import_zod.z.string().min(1).optional(),
139
- projectPath: import_zod.z.string().min(1).optional(),
138
+ project: import_zod.z.string().min(1).max(255).optional(),
139
+ projectPath: import_zod.z.string().min(1).max(4096).optional(),
140
140
  duration: import_zod.z.number().int().min(0).optional(),
141
- toolName: import_zod.z.string().min(1).optional(),
141
+ toolName: import_zod.z.string().min(1).max(100).optional(),
142
142
  toolInput: ToolInputSchema.optional(),
143
- activeFile: import_zod.z.string().min(1).optional(),
144
- model: import_zod.z.string().min(1).optional(),
143
+ activeFile: import_zod.z.string().min(1).max(4096).optional(),
144
+ model: import_zod.z.string().min(1).max(200).optional(),
145
145
  tokensUsed: import_zod.z.number().int().min(0).optional(),
146
- errorMessage: import_zod.z.string().min(1).optional(),
146
+ errorMessage: import_zod.z.string().min(1).max(1e4).optional(),
147
147
  errorType: ErrorTypeSchema.optional(),
148
148
  raw: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
149
- terminal: import_zod.z.string().min(1).optional(),
150
- cwd: import_zod.z.string().min(1).optional(),
149
+ terminal: import_zod.z.string().min(1).max(100).optional(),
150
+ cwd: import_zod.z.string().min(1).max(4096).optional(),
151
151
  pid: import_zod.z.number().int().positive().optional(),
152
- instanceId: import_zod.z.string().min(1).optional(),
152
+ instanceId: import_zod.z.string().min(1).max(255).optional(),
153
153
  instanceIndex: import_zod.z.number().int().min(1).optional(),
154
154
  instanceTotal: import_zod.z.number().int().min(1).optional()
155
155
  });
156
156
  var AISnitchEventSchema = import_zod.z.strictObject({
157
157
  specversion: import_zod.z.literal("1.0"),
158
158
  id: import_zod.z.string().refine(isUuidV7, "id must be a valid UUIDv7 string"),
159
- source: import_zod.z.string().refine(
159
+ source: import_zod.z.string().max(2e3).refine(
160
160
  isValidUriReference,
161
161
  "source must be a valid non-empty CloudEvents URI-reference"
162
162
  ),
163
163
  type: AISnitchEventTypeSchema,
164
164
  time: ISO_TIMESTAMP_SCHEMA,
165
165
  "aisnitch.tool": ToolNameSchema,
166
- "aisnitch.sessionid": import_zod.z.string().min(1),
166
+ "aisnitch.sessionid": import_zod.z.string().min(1).max(500),
167
167
  "aisnitch.seqnum": import_zod.z.number().int().min(1),
168
168
  data: EventDataSchema
169
169
  });
@@ -3351,6 +3351,346 @@ function isPidRunning(pid) {
3351
3351
  }
3352
3352
  }
3353
3353
 
3354
+ // src/core/errors.ts
3355
+ var AISnitchError = class _AISnitchError extends Error {
3356
+ /**
3357
+ * Machine-readable error code for programmatic handling.
3358
+ * Format: `SUBCATEGORY_SPECIFIC_DETAIL` (uppercase with underscores).
3359
+ */
3360
+ code;
3361
+ /**
3362
+ * Arbitrary context bag forwarded to the logger for structured debugging.
3363
+ */
3364
+ context;
3365
+ constructor(message, code, context) {
3366
+ super(message);
3367
+ this.name = "AISnitchError";
3368
+ this.code = code;
3369
+ this.context = context;
3370
+ if (Error.captureStackTrace) {
3371
+ Error.captureStackTrace(this, _AISnitchError);
3372
+ }
3373
+ }
3374
+ /**
3375
+ * Full error chain for logging: `[name] code — message`.
3376
+ */
3377
+ toString() {
3378
+ return `${this.name} [${this.code}] \u2014 ${this.message}`;
3379
+ }
3380
+ /**
3381
+ * JSON serialization friendly to pino serializers.
3382
+ */
3383
+ toJSON() {
3384
+ return {
3385
+ name: this.name,
3386
+ code: this.code,
3387
+ message: this.message,
3388
+ context: this.context,
3389
+ stack: this.stack
3390
+ };
3391
+ }
3392
+ };
3393
+ function isAISnitchError(error) {
3394
+ return error instanceof AISnitchError;
3395
+ }
3396
+ function isRetryableError(error) {
3397
+ if (!isAISnitchError(error)) {
3398
+ if (error instanceof Error) {
3399
+ const code = error.code;
3400
+ const retryableCodes = /* @__PURE__ */ new Set([
3401
+ "ECONNREFUSED",
3402
+ "ECONNRESET",
3403
+ "ETIMEDOUT",
3404
+ "ENOTFOUND",
3405
+ "EHOSTUNREACH",
3406
+ "EPIPE",
3407
+ "EPERM"
3408
+ // sometimes transient on macOS file locks
3409
+ ]);
3410
+ if (typeof code === "string" && retryableCodes.has(code)) {
3411
+ return true;
3412
+ }
3413
+ }
3414
+ return false;
3415
+ }
3416
+ const retryableCategories = /* @__PURE__ */ new Set(["TIMEOUT", "NETWORK"]);
3417
+ for (const category of retryableCategories) {
3418
+ if (error.code.startsWith(category)) {
3419
+ return true;
3420
+ }
3421
+ }
3422
+ const retryablePatterns = [
3423
+ /^ADAPTER_.*_(FILE_IO|NETWORK|PROCESS_DETECT)_ERROR$/,
3424
+ /^PIPELINE_.*_(RETRY|RECONNECT)_ERROR$/
3425
+ ];
3426
+ for (const pattern of retryablePatterns) {
3427
+ if (pattern.test(error.code)) {
3428
+ return true;
3429
+ }
3430
+ }
3431
+ return false;
3432
+ }
3433
+
3434
+ // src/core/circuit-breaker.ts
3435
+ var CircuitOpenError = class _CircuitOpenError extends AISnitchError {
3436
+ constructor(circuitId, state) {
3437
+ super(
3438
+ `Circuit "${circuitId}" is OPEN \u2014 operation rejected`,
3439
+ "CIRCUIT_OPEN",
3440
+ { circuitId, failures: state.failures, lastFailureAt: state.lastFailureAt }
3441
+ );
3442
+ this.circuitId = circuitId;
3443
+ this.state = state;
3444
+ this.name = "CircuitOpenError";
3445
+ if (Error.captureStackTrace) {
3446
+ Error.captureStackTrace(this, _CircuitOpenError);
3447
+ }
3448
+ }
3449
+ toString() {
3450
+ return `${this.name} [${this.code}] "${this.circuitId}" \u2014 failures=${this.state.failures}`;
3451
+ }
3452
+ };
3453
+ var DEFAULT_OPTIONS = {
3454
+ failureThreshold: 5,
3455
+ halfOpenAfterMs: 3e4,
3456
+ id: "unnamed",
3457
+ resetOnSuccess: true,
3458
+ shouldCountAsFailure: isRetryableError,
3459
+ windowMs: 6e4
3460
+ };
3461
+ var CircuitBreaker = class {
3462
+ failures = 0;
3463
+ lastFailureAt = null;
3464
+ state = "closed";
3465
+ halfOpenTestStartedAt = null;
3466
+ options;
3467
+ constructor(options = {}) {
3468
+ this.options = {
3469
+ ...DEFAULT_OPTIONS,
3470
+ ...options,
3471
+ // Re-spread to ensure all fields have defaults
3472
+ failureThreshold: options.failureThreshold ?? DEFAULT_OPTIONS.failureThreshold,
3473
+ halfOpenAfterMs: options.halfOpenAfterMs ?? DEFAULT_OPTIONS.halfOpenAfterMs,
3474
+ id: options.id ?? DEFAULT_OPTIONS.id,
3475
+ resetOnSuccess: options.resetOnSuccess ?? DEFAULT_OPTIONS.resetOnSuccess,
3476
+ shouldCountAsFailure: options.shouldCountAsFailure ?? DEFAULT_OPTIONS.shouldCountAsFailure,
3477
+ windowMs: options.windowMs ?? DEFAULT_OPTIONS.windowMs
3478
+ };
3479
+ }
3480
+ /**
3481
+ * Executes an async operation through the circuit breaker.
3482
+ *
3483
+ * - If the circuit is CLOSED → runs `fn` and updates state based on result
3484
+ * - If the circuit is HALF-OPEN → runs `fn` once to test recovery
3485
+ * - If the circuit is OPEN → throws `CircuitOpenError` immediately (no call)
3486
+ *
3487
+ * @param fn - The async operation to protect
3488
+ * @returns The result of `fn` if successful
3489
+ * @throws CircuitOpenError if the circuit is OPEN
3490
+ * @throws The error from `fn` if it throws (and `shouldCountAsFailure` returns true)
3491
+ */
3492
+ async execute(fn) {
3493
+ switch (this.state) {
3494
+ case "closed":
3495
+ return this.executeClosed(fn);
3496
+ case "half-open":
3497
+ return this.executeHalfOpen(fn);
3498
+ case "open":
3499
+ if (this.shouldTransitionToHalfOpen()) {
3500
+ this.transitionToHalfOpen();
3501
+ return this.executeHalfOpen(fn);
3502
+ }
3503
+ throw new CircuitOpenError(this.options.id, this.getState());
3504
+ }
3505
+ }
3506
+ /**
3507
+ * Returns the current observable circuit state.
3508
+ */
3509
+ getState() {
3510
+ return {
3511
+ failures: this.failures,
3512
+ lastFailureAt: this.lastFailureAt,
3513
+ state: this.state
3514
+ };
3515
+ }
3516
+ /**
3517
+ * Forces the circuit to CLOSED (resets failure count and state).
3518
+ * Useful for manual recovery after a known-fix or after a maintenance window.
3519
+ */
3520
+ reset() {
3521
+ this.failures = 0;
3522
+ this.lastFailureAt = null;
3523
+ this.state = "closed";
3524
+ this.halfOpenTestStartedAt = null;
3525
+ logger.debug({ circuitId: this.options.id }, "Circuit breaker manually reset");
3526
+ }
3527
+ /**
3528
+ * Pre-warms the circuit by performing one test call in HALF-OPEN state.
3529
+ * If the circuit is already HALF-OPEN, this does nothing.
3530
+ * If the circuit is CLOSED, this does nothing.
3531
+ */
3532
+ async preWarm(fn) {
3533
+ if (this.state !== "open") {
3534
+ return;
3535
+ }
3536
+ this.transitionToHalfOpen();
3537
+ try {
3538
+ await fn();
3539
+ this.transitionToClosed();
3540
+ } catch {
3541
+ this.transitionToOpen();
3542
+ }
3543
+ }
3544
+ // ─────────────────────────────────────────────────────────────────────────
3545
+ // Private methods
3546
+ // ─────────────────────────────────────────────────────────────────────────
3547
+ async executeClosed(fn) {
3548
+ try {
3549
+ const result = await fn();
3550
+ this.onSuccess();
3551
+ return result;
3552
+ } catch (error) {
3553
+ this.onFailure(error);
3554
+ throw error;
3555
+ }
3556
+ }
3557
+ async executeHalfOpen(fn) {
3558
+ this.halfOpenTestStartedAt = Date.now();
3559
+ try {
3560
+ const result = await fn();
3561
+ this.transitionToClosed();
3562
+ return result;
3563
+ } catch (error) {
3564
+ this.transitionToOpen();
3565
+ throw error;
3566
+ }
3567
+ }
3568
+ onSuccess() {
3569
+ if (this.options.resetOnSuccess) {
3570
+ this.failures = 0;
3571
+ this.lastFailureAt = null;
3572
+ } else {
3573
+ this.failures = Math.max(0, this.failures - 1);
3574
+ if (this.failures === 0) {
3575
+ this.lastFailureAt = null;
3576
+ }
3577
+ }
3578
+ logger.debug(
3579
+ {
3580
+ circuitId: this.options.id,
3581
+ failures: this.failures
3582
+ },
3583
+ "Circuit breaker operation succeeded"
3584
+ );
3585
+ }
3586
+ onFailure(error) {
3587
+ if (!this.options.shouldCountAsFailure(error)) {
3588
+ logger.debug(
3589
+ { circuitId: this.options.id, error },
3590
+ "Circuit breaker operation failed but error is not counted as failure"
3591
+ );
3592
+ return;
3593
+ }
3594
+ this.failures += 1;
3595
+ this.lastFailureAt = Date.now();
3596
+ if (this.failures >= this.options.failureThreshold) {
3597
+ this.transitionToOpen();
3598
+ } else {
3599
+ logger.debug(
3600
+ {
3601
+ circuitId: this.options.id,
3602
+ failures: this.failures,
3603
+ threshold: this.options.failureThreshold
3604
+ },
3605
+ "Circuit breaker recorded failure"
3606
+ );
3607
+ }
3608
+ }
3609
+ transitionToOpen() {
3610
+ if (this.state === "open") {
3611
+ return;
3612
+ }
3613
+ this.state = "open";
3614
+ this.halfOpenTestStartedAt = null;
3615
+ logger.warn(
3616
+ {
3617
+ circuitId: this.options.id,
3618
+ failures: this.failures,
3619
+ windowMs: this.options.windowMs
3620
+ },
3621
+ "\u{1F534} Circuit breaker OPEN \u2014 blocking operations"
3622
+ );
3623
+ }
3624
+ transitionToHalfOpen() {
3625
+ this.state = "half-open";
3626
+ this.halfOpenTestStartedAt = Date.now();
3627
+ logger.info(
3628
+ { circuitId: this.options.id },
3629
+ "\u{1F7E1} Circuit breaker HALF-OPEN \u2014 testing recovery"
3630
+ );
3631
+ }
3632
+ transitionToClosed() {
3633
+ this.state = "closed";
3634
+ this.failures = 0;
3635
+ this.lastFailureAt = null;
3636
+ this.halfOpenTestStartedAt = null;
3637
+ logger.info(
3638
+ { circuitId: this.options.id },
3639
+ "\u{1F7E2} Circuit breaker CLOSED \u2014 recovery successful"
3640
+ );
3641
+ }
3642
+ shouldTransitionToHalfOpen() {
3643
+ if (this.lastFailureAt === null) {
3644
+ return true;
3645
+ }
3646
+ const elapsed = Date.now() - this.lastFailureAt;
3647
+ return elapsed >= this.options.halfOpenAfterMs;
3648
+ }
3649
+ };
3650
+ var SHARED_BREAKERS = Object.freeze({
3651
+ /**
3652
+ * Breaker for adapter event emission.
3653
+ * Threshold: 5 failures in 60s → open for 30s → half-open test.
3654
+ */
3655
+ adapterEmit: new CircuitBreaker({
3656
+ id: "adapter.emit",
3657
+ failureThreshold: 5,
3658
+ halfOpenAfterMs: 3e4,
3659
+ shouldCountAsFailure: isRetryableError,
3660
+ windowMs: 6e4
3661
+ }),
3662
+ /**
3663
+ * Breaker for file system operations (transcript reading, config loading).
3664
+ * More tolerant: 10 failures in 60s → open for 30s.
3665
+ */
3666
+ fileSystem: new CircuitBreaker({
3667
+ id: "filesystem",
3668
+ failureThreshold: 10,
3669
+ halfOpenAfterMs: 3e4,
3670
+ windowMs: 6e4
3671
+ }),
3672
+ /**
3673
+ * Breaker for HTTP/HTTPS requests.
3674
+ * Stricter: 3 failures in 30s → open for 15s.
3675
+ */
3676
+ httpRequest: new CircuitBreaker({
3677
+ id: "http-request",
3678
+ failureThreshold: 3,
3679
+ halfOpenAfterMs: 15e3,
3680
+ windowMs: 3e4
3681
+ }),
3682
+ /**
3683
+ * Breaker for process detection operations.
3684
+ * Most tolerant: 20 failures in 60s → open for 10s.
3685
+ */
3686
+ processDetection: new CircuitBreaker({
3687
+ id: "process-detection",
3688
+ failureThreshold: 20,
3689
+ halfOpenAfterMs: 1e4,
3690
+ windowMs: 6e4
3691
+ })
3692
+ });
3693
+
3354
3694
  // src/core/engine/event-bus.ts
3355
3695
  var import_eventemitter3 = require("eventemitter3");
3356
3696
  var EventBus = class {
@@ -11116,6 +11456,50 @@ var Pipeline = class {
11116
11456
  }
11117
11457
  };
11118
11458
 
11459
+ // src/core/timeout.ts
11460
+ var DEFAULT_TIMEOUTS = Object.freeze({
11461
+ /**
11462
+ * File read/write operations (JSONL transcripts, config files).
11463
+ * Default: 5 seconds
11464
+ */
11465
+ fileOperation: 5e3,
11466
+ /**
11467
+ * HTTP requests to health endpoint or external APIs.
11468
+ * Default: 30 seconds
11469
+ */
11470
+ httpRequest: 3e4,
11471
+ /**
11472
+ * Process detection commands (`pgrep`, `ps aux`).
11473
+ * Default: 3 seconds
11474
+ */
11475
+ processDetection: 3e3,
11476
+ /**
11477
+ * Adapter startup (file watchers, hook bridges, pollers).
11478
+ * Default: 10 seconds
11479
+ */
11480
+ adapterStartup: 1e4,
11481
+ /**
11482
+ * Adapter shutdown (graceful cleanup, watcher close).
11483
+ * Default: 5 seconds — after this, resources are force-closed
11484
+ */
11485
+ adapterShutdown: 5e3,
11486
+ /**
11487
+ * Daemon graceful shutdown (stop all components in order).
11488
+ * Default: 30 seconds
11489
+ */
11490
+ daemonShutdown: 3e4,
11491
+ /**
11492
+ * WebSocket connection establishment.
11493
+ * Default: 10 seconds
11494
+ */
11495
+ wsConnection: 1e4,
11496
+ /**
11497
+ * Overall pipeline start (all components).
11498
+ * Default: 15 seconds
11499
+ */
11500
+ pipelineStartup: 15e3
11501
+ });
11502
+
11119
11503
  // src/tui/index.tsx
11120
11504
  var import_ink13 = require("ink");
11121
11505
  var import_fullscreen_ink = require("fullscreen-ink");