@hasna/instructions 0.4.33 → 0.4.35

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
@@ -47,7 +47,7 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
- // node_modules/commander/lib/error.js
50
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/error.js
51
51
  var require_error = __commonJS((exports) => {
52
52
  class CommanderError extends Error {
53
53
  constructor(exitCode, code, message) {
@@ -71,7 +71,7 @@ var require_error = __commonJS((exports) => {
71
71
  exports.InvalidArgumentError = InvalidArgumentError;
72
72
  });
73
73
 
74
- // node_modules/commander/lib/argument.js
74
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/argument.js
75
75
  var require_argument = __commonJS((exports) => {
76
76
  var { InvalidArgumentError } = require_error();
77
77
 
@@ -150,7 +150,7 @@ var require_argument = __commonJS((exports) => {
150
150
  exports.humanReadableArgName = humanReadableArgName;
151
151
  });
152
152
 
153
- // node_modules/commander/lib/help.js
153
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/help.js
154
154
  var require_help = __commonJS((exports) => {
155
155
  var { humanReadableArgName } = require_argument();
156
156
 
@@ -500,7 +500,7 @@ ${itemIndentStr}`);
500
500
  exports.stripColor = stripColor;
501
501
  });
502
502
 
503
- // node_modules/commander/lib/option.js
503
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/option.js
504
504
  var require_option = __commonJS((exports) => {
505
505
  var { InvalidArgumentError } = require_error();
506
506
 
@@ -678,7 +678,7 @@ var require_option = __commonJS((exports) => {
678
678
  exports.DualOptions = DualOptions;
679
679
  });
680
680
 
681
- // node_modules/commander/lib/suggestSimilar.js
681
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/suggestSimilar.js
682
682
  var require_suggestSimilar = __commonJS((exports) => {
683
683
  var maxDistance = 3;
684
684
  function editDistance(a, b) {
@@ -751,7 +751,7 @@ var require_suggestSimilar = __commonJS((exports) => {
751
751
  exports.suggestSimilar = suggestSimilar;
752
752
  });
753
753
 
754
- // node_modules/commander/lib/command.js
754
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/command.js
755
755
  var require_command = __commonJS((exports) => {
756
756
  var EventEmitter = __require("events").EventEmitter;
757
757
  var childProcess = __require("child_process");
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- }
996
+ } else {}
997
997
  };
998
998
  }
999
999
  return this;
@@ -2061,7 +2061,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2061
2061
  exports.useColor = useColor;
2062
2062
  });
2063
2063
 
2064
- // node_modules/commander/index.js
2064
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/index.js
2065
2065
  var require_commander = __commonJS((exports) => {
2066
2066
  var { Argument } = require_argument();
2067
2067
  var { Command } = require_command();
@@ -2136,6 +2136,30 @@ var init_types = __esm(() => {
2136
2136
  };
2137
2137
  });
2138
2138
 
2139
+ // src/lib/retired-storage-mode.ts
2140
+ function firstDefinedEnvKey(env, keys) {
2141
+ for (const key of keys) {
2142
+ if (Object.hasOwn(env, key) && env[key] !== undefined)
2143
+ return key;
2144
+ }
2145
+ return null;
2146
+ }
2147
+ function assertNoLegacyStorageMode(env = process.env) {
2148
+ const legacyKey = firstDefinedEnvKey(env, LEGACY_STORAGE_MODE_KEYS);
2149
+ if (!legacyKey)
2150
+ return;
2151
+ throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client uses the local SQLite store, or the HTTP API selected by ` + `HASNA_INSTRUCTIONS_API_URL + HASNA_INSTRUCTIONS_API_KEY. ` + `On the server, set HASNA_INSTRUCTIONS_DATABASE_URL to select the postgresql backend, ` + `or leave it unset for sqlite.`);
2152
+ }
2153
+ var LEGACY_STORAGE_MODE_KEYS;
2154
+ var init_retired_storage_mode = __esm(() => {
2155
+ LEGACY_STORAGE_MODE_KEYS = [
2156
+ "HASNA_INSTRUCTIONS_STORAGE_MODE",
2157
+ "HASNA_INSTRUCTIONS_MODE",
2158
+ "INSTRUCTIONS_STORAGE_MODE",
2159
+ "INSTRUCTIONS_MODE"
2160
+ ];
2161
+ });
2162
+
2139
2163
  // src/db/database.ts
2140
2164
  import { Database } from "bun:sqlite";
2141
2165
  import { existsSync as existsSync2, mkdirSync, rmSync } from "fs";
@@ -2162,8 +2186,9 @@ function slugify(name) {
2162
2186
  function getDatabase(path) {
2163
2187
  if (_db)
2164
2188
  return _db;
2189
+ assertNoLegacyStorageMode();
2165
2190
  if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
2166
- throw new Error("instructions is in self_hosted (cloud) mode: this command is not wired to the cloud API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
2191
+ throw new Error("instructions is using the HTTP API transport (HASNA_INSTRUCTIONS_API_URL set): this command is not wired to the API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
2167
2192
  }
2168
2193
  const dbPath = path || getDbPath();
2169
2194
  const db = new Database(dbPath);
@@ -2244,6 +2269,7 @@ function insertFeedback(input, db) {
2244
2269
  }
2245
2270
  var MIGRATIONS, _db = null;
2246
2271
  var init_database = __esm(() => {
2272
+ init_retired_storage_mode();
2247
2273
  MIGRATIONS = [
2248
2274
  `
2249
2275
  CREATE TABLE IF NOT EXISTS configs (
@@ -3037,7 +3063,7 @@ Policy reference: \`${CODEWITH_SHARED_TODOS_STORAGE_POLICY_REFERENCE}\`
3037
3063
  `;
3038
3064
  });
3039
3065
 
3040
- // node_modules/zod/v3/helpers/util.js
3066
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
3041
3067
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
3042
3068
  const t = typeof data;
3043
3069
  switch (t) {
@@ -3168,7 +3194,7 @@ var init_util = __esm(() => {
3168
3194
  ]);
3169
3195
  });
3170
3196
 
3171
- // node_modules/zod/v3/ZodError.js
3197
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
3172
3198
  var ZodIssueCode, quotelessJson = (obj) => {
3173
3199
  const json = JSON.stringify(obj, null, 2);
3174
3200
  return json.replace(/"([^"]+)":/g, "$1:");
@@ -3289,7 +3315,7 @@ var init_ZodError = __esm(() => {
3289
3315
  };
3290
3316
  });
3291
3317
 
3292
- // node_modules/zod/v3/locales/en.js
3318
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
3293
3319
  var errorMap = (issue, _ctx) => {
3294
3320
  let message;
3295
3321
  switch (issue.code) {
@@ -3396,7 +3422,7 @@ var init_en = __esm(() => {
3396
3422
  en_default = errorMap;
3397
3423
  });
3398
3424
 
3399
- // node_modules/zod/v3/errors.js
3425
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
3400
3426
  function setErrorMap(map) {
3401
3427
  overrideErrorMap = map;
3402
3428
  }
@@ -3409,7 +3435,7 @@ var init_errors = __esm(() => {
3409
3435
  overrideErrorMap = en_default;
3410
3436
  });
3411
3437
 
3412
- // node_modules/zod/v3/helpers/parseUtil.js
3438
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
3413
3439
  function addIssueToContext(ctx, issueData) {
3414
3440
  const overrideMap = getErrorMap();
3415
3441
  const issue = makeIssue({
@@ -3514,10 +3540,10 @@ var init_parseUtil = __esm(() => {
3514
3540
  });
3515
3541
  });
3516
3542
 
3517
- // node_modules/zod/v3/helpers/typeAliases.js
3543
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
3518
3544
  var init_typeAliases = () => {};
3519
3545
 
3520
- // node_modules/zod/v3/helpers/errorUtil.js
3546
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
3521
3547
  var errorUtil;
3522
3548
  var init_errorUtil = __esm(() => {
3523
3549
  (function(errorUtil2) {
@@ -3526,7 +3552,7 @@ var init_errorUtil = __esm(() => {
3526
3552
  })(errorUtil || (errorUtil = {}));
3527
3553
  });
3528
3554
 
3529
- // node_modules/zod/v3/types.js
3555
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
3530
3556
  class ParseInputLazyPath {
3531
3557
  constructor(parent, value, path, key) {
3532
3558
  this._cachedPath = [];
@@ -6877,7 +6903,7 @@ var init_types2 = __esm(() => {
6877
6903
  NEVER = INVALID;
6878
6904
  });
6879
6905
 
6880
- // node_modules/zod/v3/external.js
6906
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
6881
6907
  var exports_external = {};
6882
6908
  __export(exports_external, {
6883
6909
  void: () => voidType,
@@ -6997,7 +7023,7 @@ var init_external = __esm(() => {
6997
7023
  init_ZodError();
6998
7024
  });
6999
7025
 
7000
- // node_modules/zod/index.js
7026
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
7001
7027
  var init_zod = __esm(() => {
7002
7028
  init_external();
7003
7029
  init_external();
@@ -7189,7 +7215,7 @@ var init_redact = __esm(() => {
7189
7215
  VALUE_PATTERNS = [
7190
7216
  { re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
7191
7217
  { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
7192
- { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
7218
+ { re: /sk[-]ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
7193
7219
  { re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
7194
7220
  { re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
7195
7221
  { re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
@@ -13337,16 +13363,17 @@ function formatCliError(err, env = process.env) {
13337
13363
  return err instanceof Error ? err.message : String(err);
13338
13364
  }
13339
13365
  function resolveCloudConfig(env = process.env) {
13366
+ assertNoLegacyStorageMode(env);
13340
13367
  const apiUrl = env[API_URL_ENV]?.trim();
13341
13368
  const apiKey = env[API_KEY_ENV]?.trim();
13342
13369
  if (!apiUrl && !apiKey)
13343
13370
  return null;
13344
13371
  if (!apiUrl || !apiKey) {
13345
- throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the cloud API, ` + `or unset both to use the local store.`);
13372
+ throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the HTTP API, ` + `or unset both to use the local store.`);
13346
13373
  }
13347
13374
  return { apiUrl, apiKey };
13348
13375
  }
13349
- function isCloudMode(env = process.env) {
13376
+ function isApiTransport(env = process.env) {
13350
13377
  return resolveCloudConfig(env) !== null;
13351
13378
  }
13352
13379
 
@@ -13832,6 +13859,7 @@ var init_config_store = __esm(() => {
13832
13859
  init_types();
13833
13860
  init_bounded_read();
13834
13861
  init_instruction_graph();
13862
+ init_retired_storage_mode();
13835
13863
  CloudHttpError = class CloudHttpError extends Error {
13836
13864
  status;
13837
13865
  body;
@@ -15377,15 +15405,16 @@ var init_package_manager_guard = __esm(() => {
15377
15405
  TOKEN_VALUE_PATTERNS = [
15378
15406
  { re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
15379
15407
  { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
15380
- { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
15408
+ { re: /sk[-]ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
15381
15409
  { re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
15382
15410
  { re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
15383
15411
  { re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
15384
15412
  ];
15385
15413
  });
15386
15414
 
15387
- // node_modules/@hasna/events/dist/commander.js
15415
+ // ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
15388
15416
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
15417
+ import { Buffer as Buffer2 } from "buffer";
15389
15418
  import { existsSync } from "fs";
15390
15419
  import { homedir } from "os";
15391
15420
  import { join } from "path";
@@ -15401,29 +15430,83 @@ function getPathValue(input, path) {
15401
15430
  return;
15402
15431
  }, input);
15403
15432
  }
15404
- function wildcardToRegExp(pattern) {
15405
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
15406
- return new RegExp(`^${escaped}$`);
15433
+ function getFieldValues(input, path) {
15434
+ const values = [];
15435
+ const push = (value) => {
15436
+ if (!values.some((item) => Object.is(item, value)))
15437
+ values.push(value);
15438
+ };
15439
+ if (path.includes(".") && path in input)
15440
+ push(input[path]);
15441
+ const nestedValue = getPathValue(input, path);
15442
+ if (nestedValue !== undefined || !path.includes("."))
15443
+ push(nestedValue);
15444
+ return values;
15445
+ }
15446
+ function wildcardToRegExp(pattern, options = {}) {
15447
+ let body = "";
15448
+ for (let index = 0;index < pattern.length; index += 1) {
15449
+ const char = pattern[index];
15450
+ if (char === "*") {
15451
+ if (pattern[index + 1] === "*") {
15452
+ body += ".*";
15453
+ index += 1;
15454
+ } else {
15455
+ body += options.segmentSafe ? "[^/]*" : ".*";
15456
+ }
15457
+ } else {
15458
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
15459
+ }
15460
+ }
15461
+ return new RegExp(`^${body}$`);
15407
15462
  }
15408
- function matchString(value, matcher) {
15463
+ function matchString(value, matcher, options = {}) {
15409
15464
  if (matcher === undefined)
15410
15465
  return true;
15411
15466
  if (value === undefined)
15412
15467
  return false;
15413
15468
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
15414
- return matchers.some((item) => wildcardToRegExp(item).test(value));
15469
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
15415
15470
  }
15416
15471
  function matchRecord(input, matcher) {
15417
15472
  if (!matcher)
15418
15473
  return true;
15419
15474
  return Object.entries(matcher).every(([path, expected]) => {
15420
- const actual = getPathValue(input, path);
15421
- if (typeof expected === "string" || Array.isArray(expected)) {
15422
- return matchString(actual === undefined ? undefined : String(actual), expected);
15423
- }
15424
- return actual === expected;
15475
+ const actualValues = getFieldValues(input, path);
15476
+ return matchField(actualValues, expected, path);
15425
15477
  });
15426
15478
  }
15479
+ function matchField(actualValues, expected, path) {
15480
+ if (isNegativeMatcher(expected)) {
15481
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
15482
+ }
15483
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
15484
+ }
15485
+ function matchPositiveField(actual, expected, path) {
15486
+ if (typeof expected === "string" || Array.isArray(expected)) {
15487
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
15488
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
15489
+ }));
15490
+ }
15491
+ if (Array.isArray(actual)) {
15492
+ return actual.some((item) => item === expected);
15493
+ }
15494
+ return actual === expected;
15495
+ }
15496
+ function stringCandidates(actual) {
15497
+ if (actual === undefined)
15498
+ return [];
15499
+ if (Array.isArray(actual)) {
15500
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
15501
+ }
15502
+ return [String(actual)];
15503
+ }
15504
+ function isPrimitiveFieldValue(value) {
15505
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
15506
+ }
15507
+ function isNegativeMatcher(value) {
15508
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
15509
+ }
15427
15510
  function eventMatchesFilter(event, filter) {
15428
15511
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
15429
15512
  }
@@ -15436,16 +15519,29 @@ function channelMatchesEvent(channel, event) {
15436
15519
  }
15437
15520
  var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
15438
15521
  var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
15522
+ var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
15523
+ var DEFAULT_EVENT_PAGE_LIMIT = 100;
15524
+ var MAX_EVENT_PAGE_LIMIT = 1000;
15439
15525
  function getEventsDataDir(override) {
15440
15526
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
15441
15527
  }
15528
+ function getActiveEventsDirEnv() {
15529
+ if (process.env[HASNA_EVENTS_DIR_ENV])
15530
+ return HASNA_EVENTS_DIR_ENV;
15531
+ if (process.env[HASNA_EVENTS_HOME_ENV])
15532
+ return HASNA_EVENTS_HOME_ENV;
15533
+ return null;
15534
+ }
15535
+
15442
15536
  class JsonEventsStore {
15443
15537
  dataDir;
15538
+ runtime;
15444
15539
  channelsPath;
15445
15540
  eventsPath;
15446
15541
  deliveriesPath;
15447
15542
  constructor(dataDir = getEventsDataDir()) {
15448
15543
  this.dataDir = dataDir;
15544
+ this.runtime = localJsonRuntime(dataDir);
15449
15545
  this.channelsPath = join(dataDir, "channels.json");
15450
15546
  this.eventsPath = join(dataDir, "events.json");
15451
15547
  this.deliveriesPath = join(dataDir, "deliveries.json");
@@ -15493,13 +15589,58 @@ class JsonEventsStore {
15493
15589
  await this.writeJson(this.eventsPath, events);
15494
15590
  return event;
15495
15591
  }
15496
- async listEvents() {
15592
+ async appendEventOnce(event, options = {}) {
15497
15593
  await this.init();
15498
- return this.readJson(this.eventsPath, []);
15594
+ const events = await this.readJson(this.eventsPath, []);
15595
+ const dedupe = options.dedupe !== false;
15596
+ if (dedupe) {
15597
+ const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
15598
+ if (existing) {
15599
+ return {
15600
+ event: existing,
15601
+ stored: false,
15602
+ deduped: true,
15603
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
15604
+ };
15605
+ }
15606
+ }
15607
+ events.push(event);
15608
+ await this.writeJson(this.eventsPath, events);
15609
+ return {
15610
+ event,
15611
+ stored: true,
15612
+ deduped: false,
15613
+ identity: { id: event.id, dedupeKey: event.dedupeKey }
15614
+ };
15615
+ }
15616
+ async listEvents(options = {}) {
15617
+ await this.init();
15618
+ const events = await this.readJson(this.eventsPath, []);
15619
+ return queryEvents(events, options);
15620
+ }
15621
+ async listEventsPage(options = {}) {
15622
+ await this.init();
15623
+ const events = await this.readJson(this.eventsPath, []);
15624
+ const queried = queryEvents(events, {
15625
+ eventId: options.eventId,
15626
+ source: options.source,
15627
+ type: options.type
15628
+ });
15629
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
15630
+ const limit = normalizeEventPageLimit(options.limit);
15631
+ const pageEvents = queried.slice(offset, offset + limit);
15632
+ const nextOffset = offset + pageEvents.length;
15633
+ const hasMore = nextOffset < queried.length;
15634
+ return {
15635
+ events: pageEvents,
15636
+ cursor: options.cursor,
15637
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
15638
+ hasMore
15639
+ };
15499
15640
  }
15500
15641
  async findEventByIdentity(identity) {
15501
15642
  const events = await this.listEvents();
15502
- return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
15643
+ return findEventByIdentity(events, identity);
15503
15644
  }
15504
15645
  async appendDelivery(result) {
15505
15646
  await this.init();
@@ -15550,6 +15691,130 @@ class JsonEventsStore {
15550
15691
  });
15551
15692
  }
15552
15693
  }
15694
+ function localJsonRuntime(dataDir = getEventsDataDir()) {
15695
+ return {
15696
+ mode: "local-files",
15697
+ name: "json-events-store",
15698
+ remote: false,
15699
+ localFiles: true,
15700
+ localSqlite: false,
15701
+ postgres: false,
15702
+ s3: false,
15703
+ aws: false,
15704
+ durable: true,
15705
+ idempotency: "best-effort-local",
15706
+ replayCursors: true,
15707
+ description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
15708
+ };
15709
+ }
15710
+ function encodeLocalJsonEventCursor(offset, options = {}) {
15711
+ if (!Number.isInteger(offset) || offset < 0)
15712
+ throw new Error(`Invalid event cursor offset: ${offset}`);
15713
+ const payload = {
15714
+ offset,
15715
+ eventId: options.eventId,
15716
+ source: options.source,
15717
+ type: options.type
15718
+ };
15719
+ return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
15720
+ }
15721
+ function decodeLocalJsonEventCursor(cursor, options = {}) {
15722
+ if (!cursor)
15723
+ return 0;
15724
+ if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
15725
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
15726
+ const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
15727
+ let payload;
15728
+ try {
15729
+ payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
15730
+ } catch {
15731
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
15732
+ }
15733
+ const offset = payload.offset;
15734
+ if (!Number.isInteger(offset) || offset < 0)
15735
+ throw new Error(`Invalid local JSON event cursor: ${cursor}`);
15736
+ assertCursorFilter("eventId", payload.eventId, options.eventId);
15737
+ assertCursorFilter("source", payload.source, options.source);
15738
+ assertCursorFilter("type", payload.type, options.type);
15739
+ return offset;
15740
+ }
15741
+ function normalizeEventPageLimit(limit) {
15742
+ if (limit === undefined)
15743
+ return DEFAULT_EVENT_PAGE_LIMIT;
15744
+ if (!Number.isInteger(limit) || limit < 1)
15745
+ throw new Error(`Event page limit must be a positive integer, got ${limit}`);
15746
+ return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
15747
+ }
15748
+ function queryEvents(events, options) {
15749
+ let rows = events;
15750
+ if (options.eventId)
15751
+ rows = rows.filter((event) => event.id === options.eventId);
15752
+ if (options.source)
15753
+ rows = rows.filter((event) => event.source === options.source);
15754
+ if (options.type)
15755
+ rows = rows.filter((event) => event.type === options.type);
15756
+ if (options.cursor) {
15757
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
15758
+ rows = rows.slice(offset);
15759
+ }
15760
+ if (options.limit !== undefined)
15761
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
15762
+ return rows;
15763
+ }
15764
+ function assertCursorFilter(name, cursorValue, optionValue) {
15765
+ if (cursorValue !== optionValue)
15766
+ throw new Error(`Local JSON event cursor ${name} filter mismatch`);
15767
+ }
15768
+ function findEventByIdentity(events, identity) {
15769
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
15770
+ }
15771
+ async function getEventsStatus(dataDir) {
15772
+ const store = new JsonEventsStore(dataDir);
15773
+ await store.init();
15774
+ const [channels, events, deliveries] = await Promise.all([
15775
+ store.listChannels(),
15776
+ store.listEvents(),
15777
+ store.listDeliveries()
15778
+ ]);
15779
+ const transports = channels.reduce((counts, channel) => {
15780
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
15781
+ return counts;
15782
+ }, {});
15783
+ return {
15784
+ service: "events",
15785
+ schemaVersion: "1.0",
15786
+ dataDir: store.dataDir,
15787
+ storage: store.runtime,
15788
+ env: {
15789
+ primary: HASNA_EVENTS_DIR_ENV,
15790
+ fallback: HASNA_EVENTS_HOME_ENV,
15791
+ active: getActiveEventsDirEnv()
15792
+ },
15793
+ files: {
15794
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
15795
+ events: statusFile(store.dataDir, "events.json", events.length),
15796
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
15797
+ },
15798
+ counts: {
15799
+ channels: channels.length,
15800
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
15801
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
15802
+ events: events.length,
15803
+ deliveries: deliveries.length
15804
+ },
15805
+ transports,
15806
+ safety: {
15807
+ includesEventPayloads: false,
15808
+ includesWebhookSecrets: false,
15809
+ listOutputsRedactSecrets: true,
15810
+ statusOutputIsMetadataOnly: true
15811
+ }
15812
+ };
15813
+ }
15814
+ function statusFile(dataDir, fileName, records) {
15815
+ const path = join(dataDir, fileName);
15816
+ return { path, exists: existsSync(path), records };
15817
+ }
15553
15818
  var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
15554
15819
  function buildSignatureBase(timestamp, body) {
15555
15820
  return `${timestamp}.${body}`;
@@ -15564,21 +15829,27 @@ function now() {
15564
15829
  function truncate(value, max = 4096) {
15565
15830
  return value.length > max ? `${value.slice(0, max)}...` : value;
15566
15831
  }
15567
- function buildWebhookRequest(event, channel) {
15832
+ function buildWebhookRequest(event, channel, options = {}) {
15568
15833
  if (!channel.webhook)
15569
15834
  throw new Error(`Channel ${channel.id} has no webhook config`);
15835
+ for (const name of Object.keys(channel.webhook.headers ?? {})) {
15836
+ if (/^x-hasna-/i.test(name)) {
15837
+ throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
15838
+ }
15839
+ }
15570
15840
  const body = JSON.stringify(event);
15571
- const timestamp = event.time;
15841
+ const timestamp = options.timestamp ?? new Date().toISOString();
15572
15842
  const headers = {
15573
15843
  "Content-Type": "application/json",
15574
15844
  "User-Agent": "@hasna/events",
15575
15845
  "X-Hasna-Event-Id": event.id,
15576
15846
  "X-Hasna-Event-Type": event.type,
15577
- "X-Hasna-Timestamp": timestamp,
15578
- ...channel.webhook.headers
15847
+ ...channel.webhook.headers,
15848
+ "X-Hasna-Timestamp": timestamp
15579
15849
  };
15580
- if (channel.webhook.secret) {
15581
- headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
15850
+ const secret = options.secret ?? channel.webhook.secret;
15851
+ if (secret) {
15852
+ headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
15582
15853
  }
15583
15854
  return { body, headers };
15584
15855
  }
@@ -15586,7 +15857,21 @@ async function dispatchWebhook(event, channel, options = {}) {
15586
15857
  if (!channel.webhook)
15587
15858
  throw new Error(`Channel ${channel.id} has no webhook config`);
15588
15859
  const startedAt = now();
15589
- const { body, headers } = buildWebhookRequest(event, channel);
15860
+ let secret = channel.webhook.secret;
15861
+ if (channel.webhook.secretRef) {
15862
+ if (!options.secretResolver) {
15863
+ return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
15864
+ }
15865
+ try {
15866
+ secret = await options.secretResolver(channel.webhook.secretRef);
15867
+ } catch {
15868
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
15869
+ }
15870
+ if (!secret)
15871
+ return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
15872
+ }
15873
+ const timestamp = (options.now?.() ?? new Date).toISOString();
15874
+ const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
15590
15875
  const controller = new AbortController;
15591
15876
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
15592
15877
  try {
@@ -15618,6 +15903,15 @@ async function dispatchWebhook(event, channel, options = {}) {
15618
15903
  clearTimeout(timeout);
15619
15904
  }
15620
15905
  }
15906
+ function failedAttempt(startedAt, error) {
15907
+ return {
15908
+ attempt: 1,
15909
+ status: "failed",
15910
+ startedAt,
15911
+ completedAt: now(),
15912
+ error
15913
+ };
15914
+ }
15621
15915
  async function dispatchCommand(event, channel) {
15622
15916
  if (!channel.command)
15623
15917
  throw new Error(`Channel ${channel.id} has no command config`);
@@ -15706,6 +16000,90 @@ function createDeliveryResult(event, channel, attempts) {
15706
16000
  completedAt: attempts.at(-1)?.completedAt ?? now()
15707
16001
  };
15708
16002
  }
16003
+
16004
+ class EventValidationError extends Error {
16005
+ eventType;
16006
+ issues;
16007
+ constructor(eventType, issues) {
16008
+ const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
16009
+ super(`Event validation failed for type "${eventType}": ${detail}`);
16010
+ this.name = "EventValidationError";
16011
+ this.eventType = eventType;
16012
+ this.issues = issues;
16013
+ }
16014
+ }
16015
+
16016
+ class EventTypeCatalog {
16017
+ definitions = new Map;
16018
+ register(definition) {
16019
+ this.definitions.set(definition.type, definition);
16020
+ return this;
16021
+ }
16022
+ unregister(type) {
16023
+ return this.definitions.delete(type);
16024
+ }
16025
+ has(type) {
16026
+ return this.definitions.has(type);
16027
+ }
16028
+ get(type) {
16029
+ return this.definitions.get(type);
16030
+ }
16031
+ list() {
16032
+ return [...this.definitions.values()];
16033
+ }
16034
+ validateEvent(event) {
16035
+ const definition = this.definitions.get(event.type);
16036
+ if (!definition)
16037
+ return { ok: true };
16038
+ return definition.validate(event.data, event);
16039
+ }
16040
+ assertEventValid(event) {
16041
+ const result = this.validateEvent(event);
16042
+ if (!result.ok) {
16043
+ throw new EventValidationError(event.type, result.issues);
16044
+ }
16045
+ }
16046
+ }
16047
+ var defaultEventTypeCatalog = new EventTypeCatalog;
16048
+ var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
16049
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
16050
+ if (paths.length === 0)
16051
+ return event;
16052
+ const copy = structuredClone(event);
16053
+ for (const path of paths) {
16054
+ setPath(copy, path, replacement);
16055
+ }
16056
+ return copy;
16057
+ }
16058
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
16059
+ return redactValue(event, replacement);
16060
+ }
16061
+ function shouldRedactKey(key) {
16062
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
16063
+ }
16064
+ function redactValue(value, replacement) {
16065
+ if (Array.isArray(value))
16066
+ return value.map((item) => redactValue(item, replacement));
16067
+ if (!value || typeof value !== "object")
16068
+ return value;
16069
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
16070
+ key,
16071
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
16072
+ ]));
16073
+ }
16074
+ function setPath(input, path, replacement) {
16075
+ const parts = path.split(".");
16076
+ let cursor = input;
16077
+ for (const part of parts.slice(0, -1)) {
16078
+ const next = cursor[part];
16079
+ if (!next || typeof next !== "object")
16080
+ return;
16081
+ cursor = next;
16082
+ }
16083
+ const last = parts.at(-1);
16084
+ if (last && last in cursor)
16085
+ cursor[last] = replacement;
16086
+ }
15709
16087
  function createEvent(input) {
15710
16088
  return {
15711
16089
  id: input.id ?? randomUUID2(),
@@ -15726,10 +16104,18 @@ class EventsClient {
15726
16104
  store;
15727
16105
  redactors;
15728
16106
  transportOptions;
16107
+ catalog;
16108
+ validateCatalogTypes;
15729
16109
  constructor(options = {}) {
15730
16110
  this.store = options.store ?? new JsonEventsStore(options.dataDir);
15731
16111
  this.redactors = options.redactors ?? [];
15732
- this.transportOptions = { fetchImpl: options.fetchImpl };
16112
+ this.transportOptions = {
16113
+ fetchImpl: options.fetchImpl,
16114
+ secretResolver: options.secretResolver,
16115
+ now: options.now
16116
+ };
16117
+ this.catalog = options.catalog ?? defaultEventTypeCatalog;
16118
+ this.validateCatalogTypes = options.validateCatalogTypes ?? false;
15733
16119
  }
15734
16120
  async addChannel(input) {
15735
16121
  const timestamp = new Date().toISOString();
@@ -15747,18 +16133,40 @@ class EventsClient {
15747
16133
  }
15748
16134
  async emit(input, options = {}) {
15749
16135
  const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
15750
- if (options.dedupe !== false) {
15751
- const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
15752
- if (existing) {
15753
- return { event: existing, deliveries: [], deduped: true };
15754
- }
15755
- }
15756
- await this.store.appendEvent(event);
15757
- const deliveries = options.deliver === false ? [] : await this.deliver(event);
15758
- return { event, deliveries, deduped: false };
15759
- }
15760
- async listEvents() {
15761
- return this.store.listEvents();
16136
+ if (options.validate ?? this.validateCatalogTypes) {
16137
+ this.catalog.assertEventValid(event);
16138
+ }
16139
+ const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
16140
+ if (append.deduped) {
16141
+ return { event: append.event, deliveries: [], deduped: true };
16142
+ }
16143
+ const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
16144
+ return { event: append.event, deliveries, deduped: false };
16145
+ }
16146
+ async listEvents(options = {}) {
16147
+ if (Object.keys(options).length === 0)
16148
+ return this.store.listEvents();
16149
+ return queryClientEvents(await this.store.listEvents(), options);
16150
+ }
16151
+ async listEventsPage(options = {}) {
16152
+ if (this.store.listEventsPage)
16153
+ return this.store.listEventsPage(options);
16154
+ const events = queryClientEvents(await this.store.listEvents(), {
16155
+ eventId: options.eventId,
16156
+ source: options.source,
16157
+ type: options.type
16158
+ });
16159
+ const offset = decodeLocalJsonEventCursor(options.cursor, options);
16160
+ const limit = normalizeEventPageLimit(options.limit);
16161
+ const pageEvents = events.slice(offset, offset + limit);
16162
+ const nextOffset = offset + pageEvents.length;
16163
+ const hasMore = nextOffset < events.length;
16164
+ return {
16165
+ events: pageEvents,
16166
+ cursor: options.cursor,
16167
+ nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
16168
+ hasMore
16169
+ };
15762
16170
  }
15763
16171
  async listDeliveries() {
15764
16172
  return this.store.listDeliveries();
@@ -15775,7 +16183,7 @@ class EventsClient {
15775
16183
  }
15776
16184
  return deliveries;
15777
16185
  }
15778
- async testChannel(id, input = {}) {
16186
+ async matchChannel(id, input = {}) {
15779
16187
  const channel = await this.store.getChannel(id);
15780
16188
  if (!channel)
15781
16189
  throw new Error(`Channel not found: ${id}`);
@@ -15792,28 +16200,71 @@ class EventsClient {
15792
16200
  time: input.time,
15793
16201
  id: input.id
15794
16202
  });
16203
+ const matched = channelMatchesEvent(channel, event);
16204
+ return {
16205
+ channelId: channel.id,
16206
+ matched,
16207
+ event,
16208
+ filters: channel.filters,
16209
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
16210
+ };
16211
+ }
16212
+ async testChannel(id, input = {}, options = {}) {
16213
+ const channel = await this.store.getChannel(id);
16214
+ if (!channel)
16215
+ throw new Error(`Channel not found: ${id}`);
16216
+ const match = await this.matchChannel(id, input);
16217
+ const event = match.event;
16218
+ if (options.honorFilters && !match.matched) {
16219
+ const timestamp = new Date().toISOString();
16220
+ const result2 = createDeliveryResult(event, channel, [{
16221
+ attempt: 1,
16222
+ status: "skipped",
16223
+ startedAt: timestamp,
16224
+ completedAt: timestamp,
16225
+ error: match.reason
16226
+ }]);
16227
+ result2.metadata = { reason: "filter_mismatch" };
16228
+ await this.store.appendDelivery(result2);
16229
+ return result2;
16230
+ }
15795
16231
  const eventForChannel = await this.applyRedaction(event, channel);
15796
16232
  const result = await this.deliverWithRetry(eventForChannel, channel);
15797
16233
  await this.store.appendDelivery(result);
15798
16234
  return result;
15799
16235
  }
15800
16236
  async replay(options = {}) {
15801
- const events = (await this.store.listEvents()).filter((event) => {
15802
- if (options.eventId && event.id !== options.eventId)
15803
- return false;
15804
- if (options.source && event.source !== options.source)
15805
- return false;
15806
- if (options.type && event.type !== options.type)
15807
- return false;
15808
- return true;
15809
- });
16237
+ const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
15810
16238
  if (options.dryRun)
15811
- return { events, deliveries: [] };
16239
+ return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
15812
16240
  const deliveries = [];
15813
- for (const event of events) {
16241
+ for (const event of page.events) {
15814
16242
  deliveries.push(...await this.deliver(event));
15815
16243
  }
15816
- return { events, deliveries };
16244
+ return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
16245
+ }
16246
+ async appendEvent(event, options) {
16247
+ if (this.store.appendEventOnce) {
16248
+ return this.store.appendEventOnce(event, { dedupe: options.dedupe });
16249
+ }
16250
+ if (options.dedupe) {
16251
+ const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
16252
+ if (existing) {
16253
+ return {
16254
+ event: existing,
16255
+ stored: false,
16256
+ deduped: true,
16257
+ identity: { id: existing.id, dedupeKey: existing.dedupeKey }
16258
+ };
16259
+ }
16260
+ }
16261
+ const stored = await this.store.appendEvent(event);
16262
+ return {
16263
+ event: stored,
16264
+ stored: true,
16265
+ deduped: false,
16266
+ identity: { id: stored.id, dedupeKey: stored.dedupeKey }
16267
+ };
15817
16268
  }
15818
16269
  async applyRedaction(event, channel) {
15819
16270
  let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
@@ -15840,15 +16291,6 @@ class EventsClient {
15840
16291
  return createDeliveryResult(event, channel, attempts);
15841
16292
  }
15842
16293
  }
15843
- function redactPaths(event, paths, replacement = "[REDACTED]") {
15844
- if (paths.length === 0)
15845
- return event;
15846
- const copy = structuredClone(event);
15847
- for (const path of paths) {
15848
- setPath(copy, path, replacement);
15849
- }
15850
- return copy;
15851
- }
15852
16294
  function sanitizeChannelForOutput(channel) {
15853
16295
  const copy = structuredClone(channel);
15854
16296
  if (copy.webhook?.secret)
@@ -15861,34 +16303,19 @@ function sanitizeChannelForOutput(channel) {
15861
16303
  function sanitizeChannelsForOutput(channels) {
15862
16304
  return channels.map(sanitizeChannelForOutput);
15863
16305
  }
15864
- function redactSensitiveKeys(event, replacement = "[REDACTED]") {
15865
- return redactValue(event, replacement);
15866
- }
15867
- function shouldRedactKey(key) {
15868
- return /secret|token|password|api[_-]?key|authorization/i.test(key);
15869
- }
15870
- function redactValue(value, replacement) {
15871
- if (Array.isArray(value))
15872
- return value.map((item) => redactValue(item, replacement));
15873
- if (!value || typeof value !== "object")
15874
- return value;
15875
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
15876
- key,
15877
- shouldRedactKey(key) ? replacement : redactValue(item, replacement)
15878
- ]));
15879
- }
15880
- function setPath(input, path, replacement) {
15881
- const parts = path.split(".");
15882
- let cursor = input;
15883
- for (const part of parts.slice(0, -1)) {
15884
- const next = cursor[part];
15885
- if (!next || typeof next !== "object")
15886
- return;
15887
- cursor = next;
15888
- }
15889
- const last = parts.at(-1);
15890
- if (last && last in cursor)
15891
- cursor[last] = replacement;
16306
+ function queryClientEvents(events, options) {
16307
+ let rows = events;
16308
+ if (options.eventId)
16309
+ rows = rows.filter((event) => event.id === options.eventId);
16310
+ if (options.source)
16311
+ rows = rows.filter((event) => event.source === options.source);
16312
+ if (options.type)
16313
+ rows = rows.filter((event) => event.type === options.type);
16314
+ if (options.cursor)
16315
+ rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
16316
+ if (options.limit !== undefined)
16317
+ rows = rows.slice(0, normalizeEventPageLimit(options.limit));
16318
+ return rows;
15892
16319
  }
15893
16320
  function normalizeTime(value) {
15894
16321
  if (!value)
@@ -15902,6 +16329,77 @@ function normalizeRetryPolicy(policy) {
15902
16329
  multiplier: Math.max(1, policy?.multiplier ?? 2)
15903
16330
  };
15904
16331
  }
16332
+ function parseFieldMatchers(values, label, typed = false) {
16333
+ if (!values?.length)
16334
+ return;
16335
+ const result = {};
16336
+ for (const value of values) {
16337
+ const parsed = parseMatcherExpression(value, label);
16338
+ const path = parsed.path;
16339
+ if (path in result)
16340
+ throw new Error(`Duplicate ${label} filter path: ${path}`);
16341
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
16342
+ result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
16343
+ }
16344
+ return result;
16345
+ }
16346
+ function parseFilterOptions(options) {
16347
+ const filter2 = {};
16348
+ if (options.source)
16349
+ filter2.source = options.source;
16350
+ if (options.type)
16351
+ filter2.type = options.type;
16352
+ if (options.subject)
16353
+ filter2.subject = options.subject;
16354
+ if (options.severity)
16355
+ filter2.severity = options.severity;
16356
+ const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
16357
+ const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
16358
+ if (Object.keys(data).length > 0)
16359
+ filter2.data = data;
16360
+ if (Object.keys(metadata).length > 0)
16361
+ filter2.metadata = metadata;
16362
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
16363
+ }
16364
+ function mergeMatchers(...records) {
16365
+ const result = {};
16366
+ for (const record of records) {
16367
+ if (!record)
16368
+ continue;
16369
+ for (const [path, value] of Object.entries(record)) {
16370
+ if (path in result)
16371
+ throw new Error(`Duplicate filter path: ${path}`);
16372
+ result[path] = value;
16373
+ }
16374
+ }
16375
+ return result;
16376
+ }
16377
+ function parseTypedMatcherValue(value, label) {
16378
+ const parsed = JSON.parse(value);
16379
+ if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
16380
+ return parsed;
16381
+ }
16382
+ throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
16383
+ }
16384
+ function parseMatcherExpression(value, label) {
16385
+ const negativeSeparator = value.indexOf("!=");
16386
+ if (negativeSeparator > 0) {
16387
+ return {
16388
+ path: value.slice(0, negativeSeparator),
16389
+ rawValue: value.slice(negativeSeparator + 2),
16390
+ negated: true
16391
+ };
16392
+ }
16393
+ const separator = value.indexOf("=");
16394
+ if (separator <= 0)
16395
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
16396
+ return {
16397
+ path: value.slice(0, separator),
16398
+ rawValue: value.slice(separator + 1),
16399
+ negated: false
16400
+ };
16401
+ }
16402
+ var DEFAULT_EVENT_LIST_LIMIT = 100;
15905
16403
  function parseJsonObject(value, fallback) {
15906
16404
  if (!value)
15907
16405
  return fallback;
@@ -15923,18 +16421,6 @@ function parseHeaders(values) {
15923
16421
  }
15924
16422
  return headers;
15925
16423
  }
15926
- function parseFilter(options) {
15927
- const filter2 = {};
15928
- if (options.source)
15929
- filter2.source = options.source;
15930
- if (options.type)
15931
- filter2.type = options.type;
15932
- if (options.subject)
15933
- filter2.subject = options.subject;
15934
- if (options.severity)
15935
- filter2.severity = options.severity;
15936
- return Object.keys(filter2).length > 0 ? [filter2] : undefined;
15937
- }
15938
16424
  function createClient(options) {
15939
16425
  if (options.createClient)
15940
16426
  return options.createClient();
@@ -15946,22 +16432,30 @@ function print(value, json, text) {
15946
16432
  else
15947
16433
  console.log(text);
15948
16434
  }
16435
+ function fail(error, json) {
16436
+ const message = error instanceof Error ? error.message : String(error);
16437
+ if (json)
16438
+ console.log(JSON.stringify({ error: message }, null, 2));
16439
+ else
16440
+ console.error(message);
16441
+ process.exitCode = 1;
16442
+ }
15949
16443
  function hasJsonOption(options) {
15950
16444
  return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
15951
16445
  }
15952
16446
  function wantsJson(actionOptions, command) {
15953
16447
  return hasJsonOption(actionOptions) || hasJsonOption(command);
15954
16448
  }
15955
- function registerWebhookCommands(program, options) {
15956
- const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
15957
- webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
16449
+ function registerChannelCommands(program, options) {
16450
+ const channels = program.command(options.channelsCommandName ?? "channels").description("Manage Hasna event channels");
16451
+ channels.command("add").description("Add or replace a channel").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
15958
16452
  const timestamp = new Date().toISOString();
15959
16453
  const channel = {
15960
16454
  id: actionOptions.id,
15961
16455
  name: actionOptions.name,
15962
16456
  enabled: !actionOptions.disabled,
15963
16457
  transport: actionOptions.transport,
15964
- filters: parseFilter(actionOptions),
16458
+ filters: parseFilterOptions(actionOptions),
15965
16459
  retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
15966
16460
  redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
15967
16461
  createdAt: timestamp,
@@ -15977,35 +16471,61 @@ function registerWebhookCommands(program, options) {
15977
16471
  const saved = await createClient(options).addChannel(channel);
15978
16472
  print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
15979
16473
  });
15980
- webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
15981
- const channels = await createClient(options).listChannels();
16474
+ channels.command("list").description("List configured channels").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16475
+ const channels2 = await createClient(options).listChannels();
15982
16476
  if (wantsJson(actionOptions, command)) {
15983
- console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
16477
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
15984
16478
  return;
15985
16479
  }
15986
- if (!channels.length) {
16480
+ if (!channels2.length) {
15987
16481
  console.log("No channels configured.");
15988
16482
  return;
15989
16483
  }
15990
- for (const channel of channels) {
16484
+ for (const channel of channels2) {
15991
16485
  console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
15992
16486
  }
15993
16487
  });
15994
- webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16488
+ channels.command("status").description("Show events channel storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16489
+ const status = await getEventsStatus(options.dataDir);
16490
+ print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
16491
+ });
16492
+ channels.command("remove").description("Remove a channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
15995
16493
  const removed = await createClient(options).removeChannel(id);
15996
16494
  print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
15997
16495
  });
15998
- webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
15999
- const result = await createClient(options).testChannel(id, {
16000
- source: options.source,
16001
- type: actionOptions.type,
16002
- subject: actionOptions.subject ?? id,
16003
- message: actionOptions.message,
16004
- data: parseJsonObject(actionOptions.data, { test: true })
16005
- });
16006
- print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
16496
+ channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16497
+ const json = wantsJson(actionOptions, command);
16498
+ try {
16499
+ const result = await createClient(options).testChannel(id, {
16500
+ source: actionOptions.source ?? options.source,
16501
+ type: actionOptions.type,
16502
+ subject: actionOptions.subject ?? id,
16503
+ message: actionOptions.message,
16504
+ data: parseJsonObject(actionOptions.data, { test: true }),
16505
+ metadata: parseJsonObject(actionOptions.metadata, {})
16506
+ }, { honorFilters: actionOptions.honorFilters });
16507
+ print(result, json, `${result.status}: ${result.channelId}`);
16508
+ } catch (error) {
16509
+ fail(error, json);
16510
+ }
16511
+ });
16512
+ channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
16513
+ const json = wantsJson(actionOptions, command);
16514
+ try {
16515
+ const result = await createClient(options).matchChannel(id, {
16516
+ source: actionOptions.source ?? options.source,
16517
+ type: actionOptions.type,
16518
+ subject: actionOptions.subject ?? id,
16519
+ message: actionOptions.message,
16520
+ data: parseJsonObject(actionOptions.data, { test: true }),
16521
+ metadata: parseJsonObject(actionOptions.metadata, {})
16522
+ });
16523
+ print(result, json, `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
16524
+ } catch (error) {
16525
+ fail(error, json);
16526
+ }
16007
16527
  });
16008
- return webhooks;
16528
+ return channels;
16009
16529
  }
16010
16530
  function registerEventCommands(program, options) {
16011
16531
  const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
@@ -16022,7 +16542,8 @@ function registerEventCommands(program, options) {
16022
16542
  }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
16023
16543
  print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
16024
16544
  });
16025
- events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16545
+ const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
16546
+ events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16026
16547
  let rows = await createClient(options).listEvents();
16027
16548
  if (actionOptions.source)
16028
16549
  rows = rows.filter((event) => event.source === actionOptions.source);
@@ -16041,19 +16562,21 @@ function registerEventCommands(program, options) {
16041
16562
  for (const event of rows)
16042
16563
  console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
16043
16564
  });
16044
- events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16565
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
16045
16566
  const result = await createClient(options).replay({
16046
16567
  eventId: actionOptions.id,
16047
16568
  source: actionOptions.source,
16048
16569
  type: actionOptions.type,
16570
+ cursor: actionOptions.cursor,
16571
+ limit: actionOptions.limit,
16049
16572
  dryRun: actionOptions.dryRun
16050
16573
  });
16051
- print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
16574
+ print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
16052
16575
  });
16053
16576
  return events;
16054
16577
  }
16055
16578
  function registerEventsCommands(program, options) {
16056
- registerWebhookCommands(program, options);
16579
+ registerChannelCommands(program, options);
16057
16580
  registerEventCommands(program, options);
16058
16581
  }
16059
16582
  function parseNumber(value) {
@@ -16066,8 +16589,12 @@ function collectValues(value, previous) {
16066
16589
  previous.push(value);
16067
16590
  return previous;
16068
16591
  }
16592
+ function replaySummary(events, deliveries, nextCursor) {
16593
+ const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
16594
+ return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
16595
+ }
16069
16596
 
16070
- // node_modules/commander/esm.mjs
16597
+ // ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
16071
16598
  var import__ = __toESM(require_commander(), 1);
16072
16599
  var {
16073
16600
  program,
@@ -18384,10 +18911,10 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
18384
18911
  });
18385
18912
  program.command("whoami").description("Show setup summary").action(async () => {
18386
18913
  const store = resolveConfigStore();
18387
- const dbPath = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
18914
+ const dbPath = isApiTransport() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
18388
18915
  const stats = await store.getConfigStats();
18389
18916
  console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
18390
- console.log(chalk.cyan(isCloudMode() ? "API:" : "DB:") + " " + dbPath);
18917
+ console.log(chalk.cyan(isApiTransport() ? "API:" : "DB:") + " " + dbPath);
18391
18918
  console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
18392
18919
  console.log();
18393
18920
  console.log(chalk.bold("By category:"));
@@ -19229,9 +19756,9 @@ DB stats:`));
19229
19756
  if (count > 0)
19230
19757
  console.log(` ${key.padEnd(18)} ${count}`);
19231
19758
  }
19232
- const location = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
19759
+ const location = isApiTransport() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
19233
19760
  console.log(chalk.dim(`
19234
- ${isCloudMode() ? "API" : "DB"}: ${location}`));
19761
+ ${isApiTransport() ? "API" : "DB"}: ${location}`));
19235
19762
  });
19236
19763
  program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
19237
19764
  const status = await getConfigsStatus(resolveConfigStore());
@@ -19272,7 +19799,7 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
19272
19799
  const store = resolveConfigStore();
19273
19800
  let issues = 0;
19274
19801
  const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
19275
- const fail = (msg) => {
19802
+ const fail2 = (msg) => {
19276
19803
  issues++;
19277
19804
  console.log(chalk.red(" \u2717 ") + msg);
19278
19805
  };
@@ -19282,9 +19809,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
19282
19809
  console.log(chalk.cyan("Known files on disk:"));
19283
19810
  for (const k of KNOWN_CONFIGS) {
19284
19811
  if (k.rulesDir) {
19285
- existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
19812
+ existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
19286
19813
  } else {
19287
- existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
19814
+ existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
19288
19815
  }
19289
19816
  }
19290
19817
  const allConfigs = await store.listConfigs();
@@ -19297,7 +19824,7 @@ Stored configs (${allConfigs.length}):`));
19297
19824
  JSON.parse(c.content);
19298
19825
  validCount++;
19299
19826
  } catch {
19300
- fail(`${c.slug}: invalid JSON`);
19827
+ fail2(`${c.slug}: invalid JSON`);
19301
19828
  }
19302
19829
  } else {
19303
19830
  validCount++;
@@ -19309,13 +19836,13 @@ Stored configs (${allConfigs.length}):`));
19309
19836
  const found = scanSecrets(c.content, c.format);
19310
19837
  secretCount += found.length;
19311
19838
  }
19312
- secretCount === 0 ? pass("No unredacted secrets") : fail(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
19839
+ secretCount === 0 ? pass("No unredacted secrets") : fail2(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
19313
19840
  const duplicateTargets = findDuplicateTargetPathGroups(allConfigs);
19314
19841
  if (duplicateTargets.length === 0) {
19315
19842
  pass("No target path is claimed by more than one config");
19316
19843
  } else {
19317
19844
  const rowCount = duplicateTargets.reduce((total, group) => total + group.configs.length, 0);
19318
- fail(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
19845
+ fail2(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
19319
19846
  for (const group of duplicateTargets) {
19320
19847
  console.log(chalk.yellow(` ${group.target_path}`));
19321
19848
  for (const c of group.configs) {
@@ -19329,7 +19856,7 @@ Stored configs (${allConfigs.length}):`));
19329
19856
  pass("No reference config name is claimed by more than one row");
19330
19857
  } else {
19331
19858
  const rowCount = duplicateReferenceNames.reduce((total, group) => total + group.configs.length, 0);
19332
- fail(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
19859
+ fail2(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
19333
19860
  for (const group of duplicateReferenceNames) {
19334
19861
  console.log(chalk.yellow(` ${group.name}`));
19335
19862
  for (const c of group.configs) {