@hasna/recordings 0.1.34 → 0.1.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
@@ -2955,7 +2955,7 @@ var require_dist = __commonJS((exports) => {
2955
2955
  function parse(stream, callback) {
2956
2956
  const parser = new parser_1.Parser;
2957
2957
  stream.on("data", (buffer) => parser.parse(buffer, callback));
2958
- return new Promise((resolve) => stream.on("end", () => resolve()));
2958
+ return new Promise((resolve2) => stream.on("end", () => resolve2()));
2959
2959
  }
2960
2960
  exports.parse = parse;
2961
2961
  });
@@ -3634,12 +3634,12 @@ var require_client = __commonJS((exports, module) => {
3634
3634
  this._connect(callback);
3635
3635
  return;
3636
3636
  }
3637
- return new this._Promise((resolve, reject) => {
3637
+ return new this._Promise((resolve2, reject) => {
3638
3638
  this._connect((error) => {
3639
3639
  if (error) {
3640
3640
  reject(error);
3641
3641
  } else {
3642
- resolve(this);
3642
+ resolve2(this);
3643
3643
  }
3644
3644
  });
3645
3645
  });
@@ -3971,8 +3971,8 @@ var require_client = __commonJS((exports, module) => {
3971
3971
  readTimeout = config.query_timeout || this.connectionParameters.query_timeout;
3972
3972
  query = new Query(config, values, callback);
3973
3973
  if (!query.callback) {
3974
- result = new this._Promise((resolve, reject) => {
3975
- query.callback = (err, res) => err ? reject(err) : resolve(res);
3974
+ result = new this._Promise((resolve2, reject) => {
3975
+ query.callback = (err, res) => err ? reject(err) : resolve2(res);
3976
3976
  }).catch((err) => {
3977
3977
  Error.captureStackTrace(err);
3978
3978
  throw err;
@@ -4047,8 +4047,8 @@ var require_client = __commonJS((exports, module) => {
4047
4047
  if (cb) {
4048
4048
  this.connection.once("end", cb);
4049
4049
  } else {
4050
- return new this._Promise((resolve) => {
4051
- this.connection.once("end", resolve);
4050
+ return new this._Promise((resolve2) => {
4051
+ this.connection.once("end", resolve2);
4052
4052
  });
4053
4053
  }
4054
4054
  }
@@ -4095,8 +4095,8 @@ var require_pg_pool = __commonJS((exports, module) => {
4095
4095
  const cb = function(err, client) {
4096
4096
  err ? rej(err) : res(client);
4097
4097
  };
4098
- const result = new Promise2(function(resolve, reject) {
4099
- res = resolve;
4098
+ const result = new Promise2(function(resolve2, reject) {
4099
+ res = resolve2;
4100
4100
  rej = reject;
4101
4101
  }).catch((err) => {
4102
4102
  Error.captureStackTrace(err);
@@ -4157,7 +4157,7 @@ var require_pg_pool = __commonJS((exports, module) => {
4157
4157
  if (typeof Promise2.try === "function") {
4158
4158
  return Promise2.try(f);
4159
4159
  }
4160
- return new Promise2((resolve) => resolve(f()));
4160
+ return new Promise2((resolve2) => resolve2(f()));
4161
4161
  }
4162
4162
  _isFull() {
4163
4163
  return this._clients.length >= this.options.max;
@@ -4533,8 +4533,8 @@ var require_query2 = __commonJS((exports, module) => {
4533
4533
  NativeQuery.prototype._getPromise = function() {
4534
4534
  if (this._promise)
4535
4535
  return this._promise;
4536
- this._promise = new Promise(function(resolve, reject) {
4537
- this._once("end", resolve);
4536
+ this._promise = new Promise(function(resolve2, reject) {
4537
+ this._once("end", resolve2);
4538
4538
  this._once("error", reject);
4539
4539
  }.bind(this));
4540
4540
  return this._promise;
@@ -4708,12 +4708,12 @@ var require_client2 = __commonJS((exports, module) => {
4708
4708
  this._connect(callback);
4709
4709
  return;
4710
4710
  }
4711
- return new this._Promise((resolve, reject) => {
4711
+ return new this._Promise((resolve2, reject) => {
4712
4712
  this._connect((error) => {
4713
4713
  if (error) {
4714
4714
  reject(error);
4715
4715
  } else {
4716
- resolve(this);
4716
+ resolve2(this);
4717
4717
  }
4718
4718
  });
4719
4719
  });
@@ -4737,8 +4737,8 @@ var require_client2 = __commonJS((exports, module) => {
4737
4737
  query = new NativeQuery(config, values, callback);
4738
4738
  if (!query.callback) {
4739
4739
  let resolveOut, rejectOut;
4740
- result = new this._Promise((resolve, reject) => {
4741
- resolveOut = resolve;
4740
+ result = new this._Promise((resolve2, reject) => {
4741
+ resolveOut = resolve2;
4742
4742
  rejectOut = reject;
4743
4743
  }).catch((err) => {
4744
4744
  Error.captureStackTrace(err);
@@ -4796,8 +4796,8 @@ var require_client2 = __commonJS((exports, module) => {
4796
4796
  }
4797
4797
  let result;
4798
4798
  if (!cb) {
4799
- result = new this._Promise(function(resolve, reject) {
4800
- cb = (err) => err ? reject(err) : resolve();
4799
+ result = new this._Promise(function(resolve2, reject) {
4800
+ cb = (err) => err ? reject(err) : resolve2();
4801
4801
  });
4802
4802
  }
4803
4803
  this.native.end(function() {
@@ -5098,7 +5098,7 @@ var init_pg_migrate = __esm(() => {
5098
5098
  var require_package = __commonJS((exports, module) => {
5099
5099
  module.exports = {
5100
5100
  name: "@hasna/recordings",
5101
- version: "0.1.34",
5101
+ version: "0.1.35",
5102
5102
  type: "module",
5103
5103
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
5104
5104
  repository: {
@@ -5148,7 +5148,7 @@ var require_package = __commonJS((exports, module) => {
5148
5148
  "LICENSE"
5149
5149
  ],
5150
5150
  dependencies: {
5151
- "@hasna/events": "^0.1.7",
5151
+ "@hasna/events": "^0.1.11",
5152
5152
  "@modelcontextprotocol/sdk": "^1.12.1",
5153
5153
  chalk: "^5.4.1",
5154
5154
  commander: "^13.1.0",
@@ -5190,29 +5190,83 @@ function getPathValue(input, path) {
5190
5190
  return;
5191
5191
  }, input);
5192
5192
  }
5193
- function wildcardToRegExp(pattern) {
5194
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
5195
- return new RegExp(`^${escaped}$`);
5193
+ function getFieldValues(input, path) {
5194
+ const values = [];
5195
+ const push = (value) => {
5196
+ if (!values.some((item) => Object.is(item, value)))
5197
+ values.push(value);
5198
+ };
5199
+ if (path.includes(".") && path in input)
5200
+ push(input[path]);
5201
+ const nestedValue = getPathValue(input, path);
5202
+ if (nestedValue !== undefined || !path.includes("."))
5203
+ push(nestedValue);
5204
+ return values;
5196
5205
  }
5197
- function matchString(value, matcher) {
5206
+ function wildcardToRegExp(pattern, options = {}) {
5207
+ let body = "";
5208
+ for (let index = 0;index < pattern.length; index += 1) {
5209
+ const char = pattern[index];
5210
+ if (char === "*") {
5211
+ if (pattern[index + 1] === "*") {
5212
+ body += ".*";
5213
+ index += 1;
5214
+ } else {
5215
+ body += options.segmentSafe ? "[^/]*" : ".*";
5216
+ }
5217
+ } else {
5218
+ body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
5219
+ }
5220
+ }
5221
+ return new RegExp(`^${body}$`);
5222
+ }
5223
+ function matchString(value, matcher, options = {}) {
5198
5224
  if (matcher === undefined)
5199
5225
  return true;
5200
5226
  if (value === undefined)
5201
5227
  return false;
5202
5228
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
5203
- return matchers.some((item) => wildcardToRegExp(item).test(value));
5229
+ return matchers.some((item) => wildcardToRegExp(item, options).test(value));
5204
5230
  }
5205
5231
  function matchRecord(input, matcher) {
5206
5232
  if (!matcher)
5207
5233
  return true;
5208
5234
  return Object.entries(matcher).every(([path, expected]) => {
5209
- const actual = getPathValue(input, path);
5210
- if (typeof expected === "string" || Array.isArray(expected)) {
5211
- return matchString(actual === undefined ? undefined : String(actual), expected);
5212
- }
5213
- return actual === expected;
5235
+ const actualValues = getFieldValues(input, path);
5236
+ return matchField(actualValues, expected, path);
5214
5237
  });
5215
5238
  }
5239
+ function matchField(actualValues, expected, path) {
5240
+ if (isNegativeMatcher(expected)) {
5241
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
5242
+ }
5243
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
5244
+ }
5245
+ function matchPositiveField(actual, expected, path) {
5246
+ if (typeof expected === "string" || Array.isArray(expected)) {
5247
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
5248
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
5249
+ }));
5250
+ }
5251
+ if (Array.isArray(actual)) {
5252
+ return actual.some((item) => item === expected);
5253
+ }
5254
+ return actual === expected;
5255
+ }
5256
+ function stringCandidates(actual) {
5257
+ if (actual === undefined)
5258
+ return [];
5259
+ if (Array.isArray(actual)) {
5260
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
5261
+ }
5262
+ return [String(actual)];
5263
+ }
5264
+ function isPrimitiveFieldValue(value) {
5265
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
5266
+ }
5267
+ function isNegativeMatcher(value) {
5268
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
5269
+ }
5216
5270
  function eventMatchesFilter(event, filter) {
5217
5271
  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);
5218
5272
  }
@@ -5228,6 +5282,13 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
5228
5282
  function getEventsDataDir(override) {
5229
5283
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
5230
5284
  }
5285
+ function getActiveEventsDirEnv() {
5286
+ if (process.env[HASNA_EVENTS_DIR_ENV])
5287
+ return HASNA_EVENTS_DIR_ENV;
5288
+ if (process.env[HASNA_EVENTS_HOME_ENV])
5289
+ return HASNA_EVENTS_HOME_ENV;
5290
+ return null;
5291
+ }
5231
5292
 
5232
5293
  class JsonEventsStore {
5233
5294
  dataDir;
@@ -5340,6 +5401,52 @@ class JsonEventsStore {
5340
5401
  });
5341
5402
  }
5342
5403
  }
5404
+ async function getEventsStatus(dataDir) {
5405
+ const store = new JsonEventsStore(dataDir);
5406
+ await store.init();
5407
+ const [channels, events, deliveries] = await Promise.all([
5408
+ store.listChannels(),
5409
+ store.listEvents(),
5410
+ store.listDeliveries()
5411
+ ]);
5412
+ const transports = channels.reduce((counts, channel) => {
5413
+ counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
5414
+ return counts;
5415
+ }, {});
5416
+ return {
5417
+ service: "events",
5418
+ schemaVersion: "1.0",
5419
+ dataDir: store.dataDir,
5420
+ env: {
5421
+ primary: HASNA_EVENTS_DIR_ENV,
5422
+ fallback: HASNA_EVENTS_HOME_ENV,
5423
+ active: getActiveEventsDirEnv()
5424
+ },
5425
+ files: {
5426
+ channels: statusFile(store.dataDir, "channels.json", channels.length),
5427
+ events: statusFile(store.dataDir, "events.json", events.length),
5428
+ deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
5429
+ },
5430
+ counts: {
5431
+ channels: channels.length,
5432
+ enabledChannels: channels.filter((channel) => channel.enabled).length,
5433
+ disabledChannels: channels.filter((channel) => !channel.enabled).length,
5434
+ events: events.length,
5435
+ deliveries: deliveries.length
5436
+ },
5437
+ transports,
5438
+ safety: {
5439
+ includesEventPayloads: false,
5440
+ includesWebhookSecrets: false,
5441
+ listOutputsRedactSecrets: true,
5442
+ statusOutputIsMetadataOnly: true
5443
+ }
5444
+ };
5445
+ }
5446
+ function statusFile(dataDir, fileName, records) {
5447
+ const path = join(dataDir, fileName);
5448
+ return { path, exists: existsSync(path), records };
5449
+ }
5343
5450
  var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
5344
5451
  function buildSignatureBase(timestamp, body) {
5345
5452
  return `${timestamp}.${body}`;
@@ -5565,7 +5672,7 @@ class EventsClient {
5565
5672
  }
5566
5673
  return deliveries;
5567
5674
  }
5568
- async testChannel(id, input = {}) {
5675
+ async matchChannel(id, input = {}) {
5569
5676
  const channel = await this.store.getChannel(id);
5570
5677
  if (!channel)
5571
5678
  throw new Error(`Channel not found: ${id}`);
@@ -5582,6 +5689,34 @@ class EventsClient {
5582
5689
  time: input.time,
5583
5690
  id: input.id
5584
5691
  });
5692
+ const matched = channelMatchesEvent(channel, event);
5693
+ return {
5694
+ channelId: channel.id,
5695
+ matched,
5696
+ event,
5697
+ filters: channel.filters,
5698
+ reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
5699
+ };
5700
+ }
5701
+ async testChannel(id, input = {}, options = {}) {
5702
+ const channel = await this.store.getChannel(id);
5703
+ if (!channel)
5704
+ throw new Error(`Channel not found: ${id}`);
5705
+ const match = await this.matchChannel(id, input);
5706
+ const event = match.event;
5707
+ if (options.honorFilters && !match.matched) {
5708
+ const timestamp = new Date().toISOString();
5709
+ const result2 = createDeliveryResult(event, channel, [{
5710
+ attempt: 1,
5711
+ status: "skipped",
5712
+ startedAt: timestamp,
5713
+ completedAt: timestamp,
5714
+ error: match.reason
5715
+ }]);
5716
+ result2.metadata = { reason: "filter_mismatch" };
5717
+ await this.store.appendDelivery(result2);
5718
+ return result2;
5719
+ }
5585
5720
  const eventForChannel = await this.applyRedaction(event, channel);
5586
5721
  const result = await this.deliverWithRetry(eventForChannel, channel);
5587
5722
  await this.store.appendDelivery(result);
@@ -5692,6 +5827,76 @@ function normalizeRetryPolicy(policy) {
5692
5827
  multiplier: Math.max(1, policy?.multiplier ?? 2)
5693
5828
  };
5694
5829
  }
5830
+ function parseFieldMatchers(values, label, typed = false) {
5831
+ if (!values?.length)
5832
+ return;
5833
+ const result = {};
5834
+ for (const value of values) {
5835
+ const parsed = parseMatcherExpression(value, label);
5836
+ const path = parsed.path;
5837
+ if (path in result)
5838
+ throw new Error(`Duplicate ${label} filter path: ${path}`);
5839
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
5840
+ result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
5841
+ }
5842
+ return result;
5843
+ }
5844
+ function parseFilterOptions(options) {
5845
+ const filter2 = {};
5846
+ if (options.source)
5847
+ filter2.source = options.source;
5848
+ if (options.type)
5849
+ filter2.type = options.type;
5850
+ if (options.subject)
5851
+ filter2.subject = options.subject;
5852
+ if (options.severity)
5853
+ filter2.severity = options.severity;
5854
+ const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
5855
+ const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
5856
+ if (Object.keys(data).length > 0)
5857
+ filter2.data = data;
5858
+ if (Object.keys(metadata).length > 0)
5859
+ filter2.metadata = metadata;
5860
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
5861
+ }
5862
+ function mergeMatchers(...records) {
5863
+ const result = {};
5864
+ for (const record of records) {
5865
+ if (!record)
5866
+ continue;
5867
+ for (const [path, value] of Object.entries(record)) {
5868
+ if (path in result)
5869
+ throw new Error(`Duplicate filter path: ${path}`);
5870
+ result[path] = value;
5871
+ }
5872
+ }
5873
+ return result;
5874
+ }
5875
+ function parseTypedMatcherValue(value, label) {
5876
+ const parsed = JSON.parse(value);
5877
+ if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
5878
+ return parsed;
5879
+ }
5880
+ throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
5881
+ }
5882
+ function parseMatcherExpression(value, label) {
5883
+ const negativeSeparator = value.indexOf("!=");
5884
+ if (negativeSeparator > 0) {
5885
+ return {
5886
+ path: value.slice(0, negativeSeparator),
5887
+ rawValue: value.slice(negativeSeparator + 2),
5888
+ negated: true
5889
+ };
5890
+ }
5891
+ const separator = value.indexOf("=");
5892
+ if (separator <= 0)
5893
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
5894
+ return {
5895
+ path: value.slice(0, separator),
5896
+ rawValue: value.slice(separator + 1),
5897
+ negated: false
5898
+ };
5899
+ }
5695
5900
  function parseJsonObject(value, fallback) {
5696
5901
  if (!value)
5697
5902
  return fallback;
@@ -5713,18 +5918,6 @@ function parseHeaders(values) {
5713
5918
  }
5714
5919
  return headers;
5715
5920
  }
5716
- function parseFilter(options) {
5717
- const filter2 = {};
5718
- if (options.source)
5719
- filter2.source = options.source;
5720
- if (options.type)
5721
- filter2.type = options.type;
5722
- if (options.subject)
5723
- filter2.subject = options.subject;
5724
- if (options.severity)
5725
- filter2.severity = options.severity;
5726
- return Object.keys(filter2).length > 0 ? [filter2] : undefined;
5727
- }
5728
5921
  function createClient(options) {
5729
5922
  if (options.createClient)
5730
5923
  return options.createClient();
@@ -5744,14 +5937,14 @@ function wantsJson(actionOptions, command) {
5744
5937
  }
5745
5938
  function registerWebhookCommands(program, options) {
5746
5939
  const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
5747
- 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) => {
5940
+ 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("--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) => {
5748
5941
  const timestamp = new Date().toISOString();
5749
5942
  const channel = {
5750
5943
  id: actionOptions.id,
5751
5944
  name: actionOptions.name,
5752
5945
  enabled: !actionOptions.disabled,
5753
5946
  transport: actionOptions.transport,
5754
- filters: parseFilter(actionOptions),
5947
+ filters: parseFilterOptions(actionOptions),
5755
5948
  retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
5756
5949
  redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
5757
5950
  createdAt: timestamp,
@@ -5781,20 +5974,36 @@ function registerWebhookCommands(program, options) {
5781
5974
  console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
5782
5975
  }
5783
5976
  });
5977
+ webhooks.command("status").description("Show events webhook storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
5978
+ const status = await getEventsStatus(options.dataDir);
5979
+ print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
5980
+ });
5784
5981
  webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
5785
5982
  const removed = await createClient(options).removeChannel(id);
5786
5983
  print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
5787
5984
  });
5788
- 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) => {
5985
+ webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/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) => {
5789
5986
  const result = await createClient(options).testChannel(id, {
5790
- source: options.source,
5987
+ source: actionOptions.source ?? options.source,
5791
5988
  type: actionOptions.type,
5792
5989
  subject: actionOptions.subject ?? id,
5793
5990
  message: actionOptions.message,
5794
- data: parseJsonObject(actionOptions.data, { test: true })
5795
- });
5991
+ data: parseJsonObject(actionOptions.data, { test: true }),
5992
+ metadata: parseJsonObject(actionOptions.metadata, {})
5993
+ }, { honorFilters: actionOptions.honorFilters });
5796
5994
  print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
5797
5995
  });
5996
+ webhooks.command("match").description("Check whether a sample event matches one subscription without delivering").argument("<id>", "Subscription/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) => {
5997
+ const result = await createClient(options).matchChannel(id, {
5998
+ source: actionOptions.source ?? options.source,
5999
+ type: actionOptions.type,
6000
+ subject: actionOptions.subject ?? id,
6001
+ message: actionOptions.message,
6002
+ data: parseJsonObject(actionOptions.data, { test: true }),
6003
+ metadata: parseJsonObject(actionOptions.metadata, {})
6004
+ });
6005
+ print(result, wantsJson(actionOptions, command), `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
6006
+ });
5798
6007
  return webhooks;
5799
6008
  }
5800
6009
  function registerEventCommands(program, options) {
@@ -5861,12 +6070,12 @@ function collectValues(value, previous) {
5861
6070
  import chalk2 from "chalk";
5862
6071
  import { spawnSync } from "child_process";
5863
6072
  import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
5864
- import { dirname as dirname2, join as pathJoin } from "path";
6073
+ import { dirname as dirname3, join as pathJoin } from "path";
5865
6074
  import { fileURLToPath } from "url";
5866
6075
 
5867
6076
  // src/lib/config.ts
5868
- import { existsSync as existsSync2, readFileSync, mkdirSync, cpSync, readdirSync, statSync } from "fs";
5869
- import { join as join2 } from "path";
6077
+ import { copyFileSync, existsSync as existsSync2, readFileSync, mkdirSync, readdirSync, statSync } from "fs";
6078
+ import { dirname, join as join2, resolve } from "path";
5870
6079
  import { homedir as homedir2 } from "os";
5871
6080
  var DEFAULT_CONFIG = {
5872
6081
  openai_api_key: "",
@@ -5960,44 +6169,53 @@ function expandEnvBackedConfig(config) {
5960
6169
  return expanded;
5961
6170
  }
5962
6171
  function findConfigFile() {
5963
- let dir = process.cwd();
5964
- const root = "/";
5965
- while (dir !== root) {
5966
- const candidate = join2(dir, ".recordings", "config.json");
5967
- if (existsSync2(candidate))
5968
- return candidate;
5969
- const parent = join2(dir, "..");
5970
- if (parent === dir)
5971
- break;
5972
- dir = parent;
5973
- }
5974
- return null;
6172
+ return findProjectRecordingsPath("config.json");
5975
6173
  }
5976
6174
  function getDataDir() {
5977
- let dir = process.cwd();
5978
- const root = "/";
5979
- while (dir !== root) {
5980
- const candidate = join2(dir, ".recordings");
5981
- if (existsSync2(candidate))
5982
- return candidate;
5983
- const parent = join2(dir, "..");
5984
- if (parent === dir)
5985
- break;
5986
- dir = parent;
5987
- }
5988
- const home = homedir2();
6175
+ const projectLocalDir = findProjectRecordingsPath();
6176
+ if (projectLocalDir)
6177
+ return projectLocalDir;
6178
+ const home = getHomeDir();
5989
6179
  const newDir = join2(home, ".hasna", "recordings");
5990
6180
  const oldDir = join2(home, ".recordings");
5991
- if (!existsSync2(newDir) && existsSync2(oldDir)) {
6181
+ if (existsSync2(oldDir)) {
5992
6182
  try {
5993
- mkdirSync(join2(home, ".hasna"), { recursive: true });
5994
- cpSync(oldDir, newDir, { recursive: true });
6183
+ mergeDirectoryContents(oldDir, newDir);
5995
6184
  } catch {}
5996
6185
  }
5997
6186
  return newDir;
5998
6187
  }
6188
+ function findProjectRecordingsPath(entry) {
6189
+ const home = resolve(getHomeDir());
6190
+ let dir = resolve(process.cwd());
6191
+ while (true) {
6192
+ if (dir !== home) {
6193
+ const candidate = entry ? join2(dir, ".recordings", entry) : join2(dir, ".recordings");
6194
+ if (existsSync2(candidate))
6195
+ return candidate;
6196
+ }
6197
+ const parent = dirname(dir);
6198
+ if (parent === dir)
6199
+ break;
6200
+ dir = parent;
6201
+ }
6202
+ return null;
6203
+ }
6204
+ function mergeDirectoryContents(sourceDir, targetDir) {
6205
+ mkdirSync(targetDir, { recursive: true });
6206
+ for (const entry of readdirSync(sourceDir)) {
6207
+ const sourcePath = join2(sourceDir, entry);
6208
+ const targetPath = join2(targetDir, entry);
6209
+ const sourceStats = statSync(sourcePath);
6210
+ if (sourceStats.isDirectory()) {
6211
+ mergeDirectoryContents(sourcePath, targetPath);
6212
+ } else if (!existsSync2(targetPath)) {
6213
+ copyFileSync(sourcePath, targetPath);
6214
+ }
6215
+ }
6216
+ }
5999
6217
  function loadSecretKey(keyName) {
6000
- const secretsPath = join2(homedir2(), ".secrets");
6218
+ const secretsPath = join2(getHomeDir(), ".secrets");
6001
6219
  if (!existsSync2(secretsPath))
6002
6220
  return "";
6003
6221
  for (const candidate of listSecretFiles(secretsPath)) {
@@ -6038,6 +6256,9 @@ function listSecretFiles(path) {
6038
6256
  return [];
6039
6257
  }
6040
6258
  }
6259
+ function getHomeDir() {
6260
+ return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
6261
+ }
6041
6262
  function ensureDataDir(config) {
6042
6263
  const { mkdirSync: mkdirSync2 } = __require("fs");
6043
6264
  mkdirSync2(config.audio_dir, { recursive: true });
@@ -6079,7 +6300,7 @@ class SqliteAdapter {
6079
6300
 
6080
6301
  // src/db/database.ts
6081
6302
  import { mkdirSync as mkdirSync2 } from "fs";
6082
- import { dirname } from "path";
6303
+ import { dirname as dirname2 } from "path";
6083
6304
  var _db = null;
6084
6305
  var _adapter = null;
6085
6306
  var MIGRATIONS = [
@@ -6165,7 +6386,7 @@ function getDatabase(dbPath) {
6165
6386
  if (_db)
6166
6387
  return _db;
6167
6388
  const path = dbPath || loadConfig().db_path;
6168
- const dir = dirname(path);
6389
+ const dir = dirname2(path);
6169
6390
  mkdirSync2(dir, { recursive: true });
6170
6391
  _adapter = new SqliteAdapter(path);
6171
6392
  _db = _adapter.raw;
@@ -6768,7 +6989,7 @@ async function processText(rawText, config, systemPrompt) {
6768
6989
  }
6769
6990
 
6770
6991
  // src/version.ts
6771
- var VERSION = "0.1.33";
6992
+ var VERSION = "0.1.35";
6772
6993
 
6773
6994
  // src/cli/storage.ts
6774
6995
  import chalk from "chalk";
@@ -7162,13 +7383,13 @@ program.command("record").description("Record from microphone, transcribe, and o
7162
7383
  } else {
7163
7384
  console.log(chalk2.blue("Recording... Press") + chalk2.yellow(" Enter ") + chalk2.blue("to stop."));
7164
7385
  audioPath = startRecording(config);
7165
- await new Promise((resolve) => {
7386
+ await new Promise((resolve2) => {
7166
7387
  process.stdin.setRawMode?.(true);
7167
7388
  process.stdin.resume();
7168
7389
  process.stdin.once("data", () => {
7169
7390
  process.stdin.setRawMode?.(false);
7170
7391
  process.stdin.pause();
7171
- resolve();
7392
+ resolve2();
7172
7393
  });
7173
7394
  });
7174
7395
  stopRecording();
@@ -8092,7 +8313,7 @@ function tccAuthValueLabel(value) {
8092
8313
  }
8093
8314
  }
8094
8315
  function findPackageRoot() {
8095
- let current = dirname2(fileURLToPath(import.meta.url));
8316
+ let current = dirname3(fileURLToPath(import.meta.url));
8096
8317
  while (true) {
8097
8318
  const packagePath = pathJoin(current, "package.json");
8098
8319
  if (existsSync5(packagePath)) {
@@ -8103,7 +8324,7 @@ function findPackageRoot() {
8103
8324
  }
8104
8325
  } catch {}
8105
8326
  }
8106
- const parent = dirname2(current);
8327
+ const parent = dirname3(current);
8107
8328
  if (parent === current) {
8108
8329
  return process.cwd();
8109
8330
  }