@hasna/recordings 0.1.30 → 0.1.31

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
@@ -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.30",
5101
+ version: "0.1.31",
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,6 +5148,7 @@ var require_package = __commonJS((exports, module) => {
5148
5148
  "LICENSE"
5149
5149
  ],
5150
5150
  dependencies: {
5151
+ "@hasna/events": "^0.1.3",
5151
5152
  "@modelcontextprotocol/sdk": "^1.12.1",
5152
5153
  chalk: "^5.4.1",
5153
5154
  commander: "^13.1.0",
@@ -5171,16 +5172,696 @@ var require_package = __commonJS((exports, module) => {
5171
5172
 
5172
5173
  // src/cli/index.ts
5173
5174
  import { Command } from "commander";
5175
+
5176
+ // node_modules/@hasna/events/dist/commander.js
5177
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
5178
+ import { existsSync } from "fs";
5179
+ import { homedir } from "os";
5180
+ import { join } from "path";
5181
+ import { createHmac, timingSafeEqual } from "crypto";
5182
+ import { randomUUID } from "crypto";
5183
+ import { spawn } from "child_process";
5184
+ import { randomUUID as randomUUID2 } from "crypto";
5185
+ function getPathValue(input, path) {
5186
+ return path.split(".").reduce((value, part) => {
5187
+ if (value && typeof value === "object" && part in value) {
5188
+ return value[part];
5189
+ }
5190
+ return;
5191
+ }, input);
5192
+ }
5193
+ function wildcardToRegExp(pattern) {
5194
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
5195
+ return new RegExp(`^${escaped}$`);
5196
+ }
5197
+ function matchString(value, matcher) {
5198
+ if (matcher === undefined)
5199
+ return true;
5200
+ if (value === undefined)
5201
+ return false;
5202
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
5203
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
5204
+ }
5205
+ function matchRecord(input, matcher) {
5206
+ if (!matcher)
5207
+ return true;
5208
+ 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;
5214
+ });
5215
+ }
5216
+ function eventMatchesFilter(event, filter) {
5217
+ 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
+ }
5219
+ function channelMatchesEvent(channel, event) {
5220
+ if (!channel.enabled)
5221
+ return false;
5222
+ if (!channel.filters || channel.filters.length === 0)
5223
+ return true;
5224
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
5225
+ }
5226
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
5227
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
5228
+ function getEventsDataDir(override) {
5229
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
5230
+ }
5231
+
5232
+ class JsonEventsStore {
5233
+ dataDir;
5234
+ channelsPath;
5235
+ eventsPath;
5236
+ deliveriesPath;
5237
+ constructor(dataDir = getEventsDataDir()) {
5238
+ this.dataDir = dataDir;
5239
+ this.channelsPath = join(dataDir, "channels.json");
5240
+ this.eventsPath = join(dataDir, "events.json");
5241
+ this.deliveriesPath = join(dataDir, "deliveries.json");
5242
+ }
5243
+ async init() {
5244
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
5245
+ await chmod(this.dataDir, 448).catch(() => {
5246
+ return;
5247
+ });
5248
+ await this.ensureArrayFile(this.channelsPath);
5249
+ await this.ensureArrayFile(this.eventsPath);
5250
+ await this.ensureArrayFile(this.deliveriesPath);
5251
+ }
5252
+ async addChannel(channel) {
5253
+ await this.init();
5254
+ const channels = await this.readJson(this.channelsPath, []);
5255
+ const index = channels.findIndex((item) => item.id === channel.id);
5256
+ if (index >= 0) {
5257
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
5258
+ } else {
5259
+ channels.push(channel);
5260
+ }
5261
+ await this.writeJson(this.channelsPath, channels);
5262
+ return index >= 0 ? channels[index] : channel;
5263
+ }
5264
+ async listChannels() {
5265
+ await this.init();
5266
+ return this.readJson(this.channelsPath, []);
5267
+ }
5268
+ async getChannel(id) {
5269
+ const channels = await this.listChannels();
5270
+ return channels.find((channel) => channel.id === id);
5271
+ }
5272
+ async removeChannel(id) {
5273
+ await this.init();
5274
+ const channels = await this.readJson(this.channelsPath, []);
5275
+ const next = channels.filter((channel) => channel.id !== id);
5276
+ await this.writeJson(this.channelsPath, next);
5277
+ return next.length !== channels.length;
5278
+ }
5279
+ async appendEvent(event) {
5280
+ await this.init();
5281
+ const events = await this.readJson(this.eventsPath, []);
5282
+ events.push(event);
5283
+ await this.writeJson(this.eventsPath, events);
5284
+ return event;
5285
+ }
5286
+ async listEvents() {
5287
+ await this.init();
5288
+ return this.readJson(this.eventsPath, []);
5289
+ }
5290
+ async findEventByIdentity(identity) {
5291
+ const events = await this.listEvents();
5292
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
5293
+ }
5294
+ async appendDelivery(result) {
5295
+ await this.init();
5296
+ const deliveries = await this.readJson(this.deliveriesPath, []);
5297
+ deliveries.push(result);
5298
+ await this.writeJson(this.deliveriesPath, deliveries);
5299
+ return result;
5300
+ }
5301
+ async listDeliveries() {
5302
+ await this.init();
5303
+ return this.readJson(this.deliveriesPath, []);
5304
+ }
5305
+ async exportData() {
5306
+ return {
5307
+ channels: await this.listChannels(),
5308
+ events: await this.listEvents(),
5309
+ deliveries: await this.listDeliveries()
5310
+ };
5311
+ }
5312
+ async ensureArrayFile(path) {
5313
+ if (!existsSync(path)) {
5314
+ await writeFile(path, `[]
5315
+ `, { encoding: "utf-8", mode: 384 });
5316
+ }
5317
+ await chmod(path, 384).catch(() => {
5318
+ return;
5319
+ });
5320
+ }
5321
+ async readJson(path, fallback) {
5322
+ try {
5323
+ const raw = await readFile(path, "utf-8");
5324
+ if (!raw.trim())
5325
+ return fallback;
5326
+ return JSON.parse(raw);
5327
+ } catch (error) {
5328
+ if (error.code === "ENOENT")
5329
+ return fallback;
5330
+ throw error;
5331
+ }
5332
+ }
5333
+ async writeJson(path, value) {
5334
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
5335
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
5336
+ `, { encoding: "utf-8", mode: 384 });
5337
+ await rename(tempPath, path);
5338
+ await chmod(path, 384).catch(() => {
5339
+ return;
5340
+ });
5341
+ }
5342
+ }
5343
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
5344
+ function buildSignatureBase(timestamp, body) {
5345
+ return `${timestamp}.${body}`;
5346
+ }
5347
+ function signPayload(secret, timestamp, body) {
5348
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
5349
+ return `sha256=${digest}`;
5350
+ }
5351
+ function now() {
5352
+ return new Date().toISOString();
5353
+ }
5354
+ function truncate(value, max = 4096) {
5355
+ return value.length > max ? `${value.slice(0, max)}...` : value;
5356
+ }
5357
+ function buildWebhookRequest(event, channel) {
5358
+ if (!channel.webhook)
5359
+ throw new Error(`Channel ${channel.id} has no webhook config`);
5360
+ const body = JSON.stringify(event);
5361
+ const timestamp = event.time;
5362
+ const headers = {
5363
+ "Content-Type": "application/json",
5364
+ "User-Agent": "@hasna/events",
5365
+ "X-Hasna-Event-Id": event.id,
5366
+ "X-Hasna-Event-Type": event.type,
5367
+ "X-Hasna-Timestamp": timestamp,
5368
+ ...channel.webhook.headers
5369
+ };
5370
+ if (channel.webhook.secret) {
5371
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
5372
+ }
5373
+ return { body, headers };
5374
+ }
5375
+ async function dispatchWebhook(event, channel, options = {}) {
5376
+ if (!channel.webhook)
5377
+ throw new Error(`Channel ${channel.id} has no webhook config`);
5378
+ const startedAt = now();
5379
+ const { body, headers } = buildWebhookRequest(event, channel);
5380
+ const controller = new AbortController;
5381
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
5382
+ try {
5383
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
5384
+ method: "POST",
5385
+ headers,
5386
+ body,
5387
+ signal: controller.signal
5388
+ });
5389
+ const responseBody = truncate(await response.text());
5390
+ return {
5391
+ attempt: 1,
5392
+ status: response.ok ? "success" : "failed",
5393
+ startedAt,
5394
+ completedAt: now(),
5395
+ responseStatus: response.status,
5396
+ responseBody,
5397
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
5398
+ };
5399
+ } catch (error) {
5400
+ return {
5401
+ attempt: 1,
5402
+ status: "failed",
5403
+ startedAt,
5404
+ completedAt: now(),
5405
+ error: error instanceof Error ? error.message : String(error)
5406
+ };
5407
+ } finally {
5408
+ clearTimeout(timeout);
5409
+ }
5410
+ }
5411
+ async function dispatchCommand(event, channel) {
5412
+ if (!channel.command)
5413
+ throw new Error(`Channel ${channel.id} has no command config`);
5414
+ const startedAt = now();
5415
+ const eventJson = JSON.stringify(event);
5416
+ const env = {
5417
+ ...process.env,
5418
+ ...channel.command.env,
5419
+ HASNA_CHANNEL_ID: channel.id,
5420
+ HASNA_EVENT_ID: event.id,
5421
+ HASNA_EVENT_TYPE: event.type,
5422
+ HASNA_EVENT_SOURCE: event.source,
5423
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
5424
+ HASNA_EVENT_SEVERITY: event.severity,
5425
+ HASNA_EVENT_TIME: event.time,
5426
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
5427
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
5428
+ HASNA_EVENT_JSON: eventJson
5429
+ };
5430
+ return new Promise((resolve) => {
5431
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
5432
+ cwd: channel.command.cwd,
5433
+ env,
5434
+ stdio: ["pipe", "pipe", "pipe"]
5435
+ });
5436
+ let stdout = "";
5437
+ let stderr = "";
5438
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
5439
+ child.stdin.end(eventJson);
5440
+ child.stdout.on("data", (chunk) => {
5441
+ stdout += chunk.toString();
5442
+ });
5443
+ child.stderr.on("data", (chunk) => {
5444
+ stderr += chunk.toString();
5445
+ });
5446
+ child.on("error", (error) => {
5447
+ clearTimeout(timeout);
5448
+ resolve({
5449
+ attempt: 1,
5450
+ status: "failed",
5451
+ startedAt,
5452
+ completedAt: now(),
5453
+ stdout: truncate(stdout),
5454
+ stderr: truncate(stderr),
5455
+ error: error.message
5456
+ });
5457
+ });
5458
+ child.on("close", (code, signal) => {
5459
+ clearTimeout(timeout);
5460
+ const success = code === 0;
5461
+ resolve({
5462
+ attempt: 1,
5463
+ status: success ? "success" : "failed",
5464
+ startedAt,
5465
+ completedAt: now(),
5466
+ stdout: truncate(stdout),
5467
+ stderr: truncate(stderr),
5468
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
5469
+ });
5470
+ });
5471
+ });
5472
+ }
5473
+ async function dispatchChannel(event, channel, options = {}) {
5474
+ if (channel.transport === "webhook")
5475
+ return dispatchWebhook(event, channel, options);
5476
+ if (channel.transport === "command")
5477
+ return dispatchCommand(event, channel);
5478
+ return {
5479
+ attempt: 1,
5480
+ status: "skipped",
5481
+ startedAt: now(),
5482
+ completedAt: now(),
5483
+ error: `Unsupported transport: ${channel.transport}`
5484
+ };
5485
+ }
5486
+ function createDeliveryResult(event, channel, attempts) {
5487
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
5488
+ return {
5489
+ id: randomUUID(),
5490
+ eventId: event.id,
5491
+ channelId: channel.id,
5492
+ transport: channel.transport,
5493
+ status,
5494
+ attempts,
5495
+ createdAt: attempts[0]?.startedAt ?? now(),
5496
+ completedAt: attempts.at(-1)?.completedAt ?? now()
5497
+ };
5498
+ }
5499
+ function createEvent(input) {
5500
+ return {
5501
+ id: input.id ?? randomUUID2(),
5502
+ source: input.source,
5503
+ type: input.type,
5504
+ time: normalizeTime(input.time),
5505
+ subject: input.subject,
5506
+ severity: input.severity ?? "info",
5507
+ data: input.data ?? {},
5508
+ message: input.message,
5509
+ dedupeKey: input.dedupeKey,
5510
+ schemaVersion: input.schemaVersion ?? "1.0",
5511
+ metadata: input.metadata ?? {}
5512
+ };
5513
+ }
5514
+
5515
+ class EventsClient {
5516
+ store;
5517
+ redactors;
5518
+ transportOptions;
5519
+ constructor(options = {}) {
5520
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
5521
+ this.redactors = options.redactors ?? [];
5522
+ this.transportOptions = { fetchImpl: options.fetchImpl };
5523
+ }
5524
+ async addChannel(input) {
5525
+ const timestamp = new Date().toISOString();
5526
+ return this.store.addChannel({
5527
+ ...input,
5528
+ createdAt: input.createdAt ?? timestamp,
5529
+ updatedAt: input.updatedAt ?? timestamp
5530
+ });
5531
+ }
5532
+ async listChannels() {
5533
+ return this.store.listChannels();
5534
+ }
5535
+ async removeChannel(id) {
5536
+ return this.store.removeChannel(id);
5537
+ }
5538
+ async emit(input, options = {}) {
5539
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
5540
+ if (options.dedupe !== false) {
5541
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
5542
+ if (existing) {
5543
+ return { event: existing, deliveries: [], deduped: true };
5544
+ }
5545
+ }
5546
+ await this.store.appendEvent(event);
5547
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
5548
+ return { event, deliveries, deduped: false };
5549
+ }
5550
+ async listEvents() {
5551
+ return this.store.listEvents();
5552
+ }
5553
+ async listDeliveries() {
5554
+ return this.store.listDeliveries();
5555
+ }
5556
+ async deliver(event) {
5557
+ const channels = await this.store.listChannels();
5558
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
5559
+ const deliveries = [];
5560
+ for (const channel of selected) {
5561
+ const eventForChannel = await this.applyRedaction(event, channel);
5562
+ const result = await this.deliverWithRetry(eventForChannel, channel);
5563
+ await this.store.appendDelivery(result);
5564
+ deliveries.push(result);
5565
+ }
5566
+ return deliveries;
5567
+ }
5568
+ async testChannel(id, input = {}) {
5569
+ const channel = await this.store.getChannel(id);
5570
+ if (!channel)
5571
+ throw new Error(`Channel not found: ${id}`);
5572
+ const event = createEvent({
5573
+ source: input.source ?? "hasna.events",
5574
+ type: input.type ?? "events.test",
5575
+ subject: input.subject ?? id,
5576
+ severity: input.severity ?? "info",
5577
+ data: input.data ?? { test: true },
5578
+ message: input.message ?? "Hasna events test delivery",
5579
+ dedupeKey: input.dedupeKey,
5580
+ schemaVersion: input.schemaVersion,
5581
+ metadata: input.metadata,
5582
+ time: input.time,
5583
+ id: input.id
5584
+ });
5585
+ const eventForChannel = await this.applyRedaction(event, channel);
5586
+ const result = await this.deliverWithRetry(eventForChannel, channel);
5587
+ await this.store.appendDelivery(result);
5588
+ return result;
5589
+ }
5590
+ async replay(options = {}) {
5591
+ const events = (await this.store.listEvents()).filter((event) => {
5592
+ if (options.eventId && event.id !== options.eventId)
5593
+ return false;
5594
+ if (options.source && event.source !== options.source)
5595
+ return false;
5596
+ if (options.type && event.type !== options.type)
5597
+ return false;
5598
+ return true;
5599
+ });
5600
+ if (options.dryRun)
5601
+ return { events, deliveries: [] };
5602
+ const deliveries = [];
5603
+ for (const event of events) {
5604
+ deliveries.push(...await this.deliver(event));
5605
+ }
5606
+ return { events, deliveries };
5607
+ }
5608
+ async applyRedaction(event, channel) {
5609
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
5610
+ for (const redactor of this.redactors) {
5611
+ next = await redactor(next, channel);
5612
+ }
5613
+ return next;
5614
+ }
5615
+ async deliverWithRetry(event, channel) {
5616
+ const policy = normalizeRetryPolicy(channel.retry);
5617
+ const attempts = [];
5618
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
5619
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
5620
+ attempt.attempt = index + 1;
5621
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
5622
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
5623
+ }
5624
+ attempts.push(attempt);
5625
+ if (attempt.status !== "failed")
5626
+ break;
5627
+ if (attempt.nextBackoffMs)
5628
+ await Bun.sleep(attempt.nextBackoffMs);
5629
+ }
5630
+ return createDeliveryResult(event, channel, attempts);
5631
+ }
5632
+ }
5633
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
5634
+ if (paths.length === 0)
5635
+ return event;
5636
+ const copy = structuredClone(event);
5637
+ for (const path of paths) {
5638
+ setPath(copy, path, replacement);
5639
+ }
5640
+ return copy;
5641
+ }
5642
+ function sanitizeChannelForOutput(channel) {
5643
+ const copy = structuredClone(channel);
5644
+ if (copy.webhook?.secret)
5645
+ copy.webhook.secret = "[REDACTED]";
5646
+ if (copy.command?.env) {
5647
+ copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
5648
+ }
5649
+ return copy;
5650
+ }
5651
+ function sanitizeChannelsForOutput(channels) {
5652
+ return channels.map(sanitizeChannelForOutput);
5653
+ }
5654
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
5655
+ return redactValue(event, replacement);
5656
+ }
5657
+ function shouldRedactKey(key) {
5658
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
5659
+ }
5660
+ function redactValue(value, replacement) {
5661
+ if (Array.isArray(value))
5662
+ return value.map((item) => redactValue(item, replacement));
5663
+ if (!value || typeof value !== "object")
5664
+ return value;
5665
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
5666
+ key,
5667
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
5668
+ ]));
5669
+ }
5670
+ function setPath(input, path, replacement) {
5671
+ const parts = path.split(".");
5672
+ let cursor = input;
5673
+ for (const part of parts.slice(0, -1)) {
5674
+ const next = cursor[part];
5675
+ if (!next || typeof next !== "object")
5676
+ return;
5677
+ cursor = next;
5678
+ }
5679
+ const last = parts.at(-1);
5680
+ if (last && last in cursor)
5681
+ cursor[last] = replacement;
5682
+ }
5683
+ function normalizeTime(value) {
5684
+ if (!value)
5685
+ return new Date().toISOString();
5686
+ return value instanceof Date ? value.toISOString() : value;
5687
+ }
5688
+ function normalizeRetryPolicy(policy) {
5689
+ return {
5690
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
5691
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
5692
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
5693
+ };
5694
+ }
5695
+ function parseJsonObject(value, fallback) {
5696
+ if (!value)
5697
+ return fallback;
5698
+ const parsed = JSON.parse(value);
5699
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5700
+ throw new Error("Expected a JSON object");
5701
+ }
5702
+ return parsed;
5703
+ }
5704
+ function parseHeaders(values) {
5705
+ if (!values?.length)
5706
+ return;
5707
+ const headers = {};
5708
+ for (const value of values) {
5709
+ const separator = value.indexOf("=");
5710
+ if (separator === -1)
5711
+ throw new Error(`Invalid header, expected name=value: ${value}`);
5712
+ headers[value.slice(0, separator)] = value.slice(separator + 1);
5713
+ }
5714
+ return headers;
5715
+ }
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
+ function createClient(options) {
5729
+ if (options.createClient)
5730
+ return options.createClient();
5731
+ return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
5732
+ }
5733
+ function print(value, json, text) {
5734
+ if (json)
5735
+ console.log(JSON.stringify(value, null, 2));
5736
+ else
5737
+ console.log(text);
5738
+ }
5739
+ function registerWebhookCommands(program, options) {
5740
+ const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
5741
+ 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) => {
5742
+ const timestamp = new Date().toISOString();
5743
+ const channel = {
5744
+ id: actionOptions.id,
5745
+ name: actionOptions.name,
5746
+ enabled: !actionOptions.disabled,
5747
+ transport: actionOptions.transport,
5748
+ filters: parseFilter(actionOptions),
5749
+ retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
5750
+ redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
5751
+ createdAt: timestamp,
5752
+ updatedAt: timestamp
5753
+ };
5754
+ if (actionOptions.transport === "webhook") {
5755
+ channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
5756
+ } else if (actionOptions.transport === "command") {
5757
+ channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
5758
+ } else {
5759
+ throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
5760
+ }
5761
+ const saved = await createClient(options).addChannel(channel);
5762
+ print(sanitizeChannelForOutput(saved), Boolean(actionOptions.json), `Added ${saved.transport} channel ${saved.id}`);
5763
+ });
5764
+ webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
5765
+ const channels = await createClient(options).listChannels();
5766
+ if (actionOptions.json) {
5767
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
5768
+ return;
5769
+ }
5770
+ if (!channels.length) {
5771
+ console.log("No channels configured.");
5772
+ return;
5773
+ }
5774
+ for (const channel of channels) {
5775
+ console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
5776
+ }
5777
+ });
5778
+ webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
5779
+ const removed = await createClient(options).removeChannel(id);
5780
+ print({ removed }, Boolean(actionOptions.json), removed ? `Removed ${id}` : `Channel not found: ${id}`);
5781
+ });
5782
+ 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) => {
5783
+ const result = await createClient(options).testChannel(id, {
5784
+ source: options.source,
5785
+ type: actionOptions.type,
5786
+ subject: actionOptions.subject ?? id,
5787
+ message: actionOptions.message,
5788
+ data: parseJsonObject(actionOptions.data, { test: true })
5789
+ });
5790
+ print(result, Boolean(actionOptions.json), `${result.status}: ${result.channelId}`);
5791
+ });
5792
+ return webhooks;
5793
+ }
5794
+ function registerEventCommands(program, options) {
5795
+ const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
5796
+ events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions) => {
5797
+ const result = await createClient(options).emit({
5798
+ source: actionOptions.source ?? options.source,
5799
+ type,
5800
+ subject: actionOptions.subject,
5801
+ severity: actionOptions.severity,
5802
+ message: actionOptions.message,
5803
+ dedupeKey: actionOptions.dedupeKey,
5804
+ data: parseJsonObject(actionOptions.data, {}),
5805
+ metadata: parseJsonObject(actionOptions.metadata, {})
5806
+ }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
5807
+ print(result, Boolean(actionOptions.json), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
5808
+ });
5809
+ 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) => {
5810
+ let rows = await createClient(options).listEvents();
5811
+ if (actionOptions.source)
5812
+ rows = rows.filter((event) => event.source === actionOptions.source);
5813
+ if (actionOptions.type)
5814
+ rows = rows.filter((event) => event.type === actionOptions.type);
5815
+ if (actionOptions.limit)
5816
+ rows = rows.slice(-actionOptions.limit);
5817
+ if (actionOptions.json) {
5818
+ console.log(JSON.stringify(rows, null, 2));
5819
+ return;
5820
+ }
5821
+ if (!rows.length) {
5822
+ console.log("No events recorded.");
5823
+ return;
5824
+ }
5825
+ for (const event of rows)
5826
+ console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
5827
+ });
5828
+ 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) => {
5829
+ const result = await createClient(options).replay({
5830
+ eventId: actionOptions.id,
5831
+ source: actionOptions.source,
5832
+ type: actionOptions.type,
5833
+ dryRun: actionOptions.dryRun
5834
+ });
5835
+ print(result, Boolean(actionOptions.json), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
5836
+ });
5837
+ return events;
5838
+ }
5839
+ function registerEventsCommands(program, options) {
5840
+ registerWebhookCommands(program, options);
5841
+ registerEventCommands(program, options);
5842
+ }
5843
+ function parseNumber(value) {
5844
+ const parsed = Number(value);
5845
+ if (!Number.isFinite(parsed))
5846
+ throw new Error(`Expected a number, got ${value}`);
5847
+ return parsed;
5848
+ }
5849
+ function collectValues(value, previous) {
5850
+ previous.push(value);
5851
+ return previous;
5852
+ }
5853
+
5854
+ // src/cli/index.ts
5174
5855
  import chalk2 from "chalk";
5175
5856
  import { spawnSync } from "child_process";
5176
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
5857
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
5177
5858
  import { dirname as dirname2, join as pathJoin } from "path";
5178
5859
  import { fileURLToPath } from "url";
5179
5860
 
5180
5861
  // src/lib/config.ts
5181
- import { existsSync, readFileSync, mkdirSync, cpSync, readdirSync, statSync } from "fs";
5182
- import { join } from "path";
5183
- import { homedir } from "os";
5862
+ import { existsSync as existsSync2, readFileSync, mkdirSync, cpSync, readdirSync, statSync } from "fs";
5863
+ import { join as join2 } from "path";
5864
+ import { homedir as homedir2 } from "os";
5184
5865
  var DEFAULT_CONFIG = {
5185
5866
  openai_api_key: "",
5186
5867
  enhancement_api_key: "",
@@ -5211,8 +5892,8 @@ var DEFAULT_CONFIG = {
5211
5892
  };
5212
5893
  function loadConfig(configPath) {
5213
5894
  const config = { ...DEFAULT_CONFIG };
5214
- const filePath = configPath || findConfigFile() || join(getDataDir(), "config.json");
5215
- if (existsSync(filePath)) {
5895
+ const filePath = configPath || findConfigFile() || join2(getDataDir(), "config.json");
5896
+ if (existsSync2(filePath)) {
5216
5897
  try {
5217
5898
  const raw = readFileSync(filePath, "utf-8");
5218
5899
  const fileConfig = JSON.parse(raw);
@@ -5255,10 +5936,10 @@ function loadConfig(configPath) {
5255
5936
  config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
5256
5937
  }
5257
5938
  if (!config.db_path) {
5258
- config.db_path = join(getDataDir(), "recordings.db");
5939
+ config.db_path = join2(getDataDir(), "recordings.db");
5259
5940
  }
5260
5941
  if (!config.audio_dir) {
5261
- config.audio_dir = join(getDataDir(), "audio");
5942
+ config.audio_dir = join2(getDataDir(), "audio");
5262
5943
  }
5263
5944
  return config;
5264
5945
  }
@@ -5276,10 +5957,10 @@ function findConfigFile() {
5276
5957
  let dir = process.cwd();
5277
5958
  const root = "/";
5278
5959
  while (dir !== root) {
5279
- const candidate = join(dir, ".recordings", "config.json");
5280
- if (existsSync(candidate))
5960
+ const candidate = join2(dir, ".recordings", "config.json");
5961
+ if (existsSync2(candidate))
5281
5962
  return candidate;
5282
- const parent = join(dir, "..");
5963
+ const parent = join2(dir, "..");
5283
5964
  if (parent === dir)
5284
5965
  break;
5285
5966
  dir = parent;
@@ -5290,28 +5971,28 @@ function getDataDir() {
5290
5971
  let dir = process.cwd();
5291
5972
  const root = "/";
5292
5973
  while (dir !== root) {
5293
- const candidate = join(dir, ".recordings");
5294
- if (existsSync(candidate))
5974
+ const candidate = join2(dir, ".recordings");
5975
+ if (existsSync2(candidate))
5295
5976
  return candidate;
5296
- const parent = join(dir, "..");
5977
+ const parent = join2(dir, "..");
5297
5978
  if (parent === dir)
5298
5979
  break;
5299
5980
  dir = parent;
5300
5981
  }
5301
- const home = homedir();
5302
- const newDir = join(home, ".hasna", "recordings");
5303
- const oldDir = join(home, ".recordings");
5304
- if (!existsSync(newDir) && existsSync(oldDir)) {
5982
+ const home = homedir2();
5983
+ const newDir = join2(home, ".hasna", "recordings");
5984
+ const oldDir = join2(home, ".recordings");
5985
+ if (!existsSync2(newDir) && existsSync2(oldDir)) {
5305
5986
  try {
5306
- mkdirSync(join(home, ".hasna"), { recursive: true });
5987
+ mkdirSync(join2(home, ".hasna"), { recursive: true });
5307
5988
  cpSync(oldDir, newDir, { recursive: true });
5308
5989
  } catch {}
5309
5990
  }
5310
5991
  return newDir;
5311
5992
  }
5312
5993
  function loadSecretKey(keyName) {
5313
- const secretsPath = join(homedir(), ".secrets");
5314
- if (!existsSync(secretsPath))
5994
+ const secretsPath = join2(homedir2(), ".secrets");
5995
+ if (!existsSync2(secretsPath))
5315
5996
  return "";
5316
5997
  for (const candidate of listSecretFiles(secretsPath)) {
5317
5998
  try {
@@ -5337,7 +6018,7 @@ function listSecretFiles(path) {
5337
6018
  if (!stats.isDirectory())
5338
6019
  return [];
5339
6020
  return readdirSync(path).sort().flatMap((entry) => {
5340
- const child = join(path, entry);
6021
+ const child = join2(path, entry);
5341
6022
  try {
5342
6023
  const childStats = statSync(child);
5343
6024
  if (childStats.isDirectory())
@@ -5702,9 +6383,9 @@ function listProjects(db) {
5702
6383
  }
5703
6384
 
5704
6385
  // src/lib/recorder.ts
5705
- import { spawn } from "child_process";
5706
- import { join as join2 } from "path";
5707
- import { existsSync as existsSync2 } from "fs";
6386
+ import { spawn as spawn2 } from "child_process";
6387
+ import { join as join3 } from "path";
6388
+ import { existsSync as existsSync3 } from "fs";
5708
6389
 
5709
6390
  // src/types/index.ts
5710
6391
  class RecordingError extends Error {
@@ -5778,9 +6459,9 @@ function startRecording(config) {
5778
6459
  }
5779
6460
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
5780
6461
  const filename = `recording-${timestamp}.${config.audio_format}`;
5781
- const filepath = join2(config.audio_dir, filename);
6462
+ const filepath = join3(config.audio_dir, filename);
5782
6463
  const args = buildRecordArgs(filepath, config);
5783
- _recordProcess = spawn(args[0], args.slice(1), {
6464
+ _recordProcess = spawn2(args[0], args.slice(1), {
5784
6465
  stdio: ["pipe", "pipe", "pipe"]
5785
6466
  });
5786
6467
  if (config.max_recording_seconds > 0) {
@@ -5833,7 +6514,7 @@ function buildRecordArgs(filepath, config) {
5833
6514
  async function recordDuration(seconds, config) {
5834
6515
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
5835
6516
  const filename = `recording-${timestamp}.${config.audio_format}`;
5836
- const filepath = join2(config.audio_dir, filename);
6517
+ const filepath = join3(config.audio_dir, filename);
5837
6518
  const args = [
5838
6519
  "rec",
5839
6520
  "-r",
@@ -5852,7 +6533,7 @@ async function recordDuration(seconds, config) {
5852
6533
  stderr: "pipe"
5853
6534
  });
5854
6535
  const exitCode = await proc.exited;
5855
- if (exitCode !== 0 && !existsSync2(filepath)) {
6536
+ if (exitCode !== 0 && !existsSync3(filepath)) {
5856
6537
  const stderr = await new Response(proc.stderr).text();
5857
6538
  throw new RecordingError(`Recording failed (exit ${exitCode}): ${stderr}`);
5858
6539
  }
@@ -6087,10 +6768,10 @@ var VERSION = "0.1.30";
6087
6768
  import chalk from "chalk";
6088
6769
 
6089
6770
  // src/db/storage-config.ts
6090
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
6091
- import { homedir as homedir2 } from "os";
6092
- import { join as join3 } from "path";
6093
- var STORAGE_CONFIG_PATH = join3(homedir2(), ".hasna", "recordings", "storage", "config.json");
6771
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
6772
+ import { homedir as homedir3 } from "os";
6773
+ import { join as join4 } from "path";
6774
+ var STORAGE_CONFIG_PATH = join4(homedir3(), ".hasna", "recordings", "storage", "config.json");
6094
6775
  var RECORDINGS_STORAGE_ENV = "HASNA_RECORDINGS_DATABASE_URL";
6095
6776
  var RECORDINGS_STORAGE_FALLBACK_ENV = "RECORDINGS_DATABASE_URL";
6096
6777
  var RECORDINGS_STORAGE_MODE_ENV = "HASNA_RECORDINGS_STORAGE_MODE";
@@ -6132,7 +6813,7 @@ function getStorageConfig() {
6132
6813
  ssl: true
6133
6814
  }
6134
6815
  };
6135
- if (existsSync3(STORAGE_CONFIG_PATH)) {
6816
+ if (existsSync4(STORAGE_CONFIG_PATH)) {
6136
6817
  try {
6137
6818
  const raw = JSON.parse(readFileSync2(STORAGE_CONFIG_PATH, "utf-8"));
6138
6819
  config.mode = normalizeMode(raw.mode) ?? config.mode;
@@ -6454,6 +7135,7 @@ function applyEnhancementOptions(config, opts) {
6454
7135
  var program = new Command;
6455
7136
  program.name("recordings").description("Speech-to-text recording tool \u2014 record, transcribe, and enhance with AI").version(VERSION).option("--json", "Output as JSON").option("--agent <name>", "Agent name or ID").option("--project <name>", "Project name or ID").option("--session <id>", "Session ID");
6456
7137
  registerStorageCommands(program);
7138
+ registerEventsCommands(program, { source: "recordings" });
6457
7139
  program.command("record").description("Record from microphone, transcribe, and optionally enhance").option("-d, --duration <seconds>", "Record for specific duration").option("--no-enhance", "Skip AI enhancement").option("-t, --tags <tags>", "Comma-separated tags").option("-l, --language <lang>", "Language code (e.g. en, es, fr)").action(async (opts) => {
6458
7140
  const config = loadConfig();
6459
7141
  ensureDataDir(config);
@@ -6725,13 +7407,13 @@ program.command("projects").description("List registered projects").action(() =>
6725
7407
  }
6726
7408
  });
6727
7409
  program.command("init").description("Initialize .recordings/ in current directory").action(() => {
6728
- const { mkdirSync: mkdirSync3, writeFileSync, existsSync: existsSync5 } = __require("fs");
6729
- const { join: join4 } = __require("path");
6730
- const dir = join4(process.cwd(), ".recordings");
6731
- const audioDir = join4(dir, "audio");
6732
- const configFile = join4(dir, "config.json");
7410
+ const { mkdirSync: mkdirSync3, writeFileSync, existsSync: existsSync6 } = __require("fs");
7411
+ const { join: join5 } = __require("path");
7412
+ const dir = join5(process.cwd(), ".recordings");
7413
+ const audioDir = join5(dir, "audio");
7414
+ const configFile = join5(dir, "config.json");
6733
7415
  mkdirSync3(audioDir, { recursive: true });
6734
- if (!existsSync5(configFile)) {
7416
+ if (!existsSync6(configFile)) {
6735
7417
  const defaultConf = {
6736
7418
  transcription_model: "gpt-4o-mini-transcribe",
6737
7419
  enhancement_model: "gpt-4o",
@@ -6834,7 +7516,7 @@ appCommand.command("request-permissions").description("Open Recordings.app and t
6834
7516
  });
6835
7517
  appCommand.command("log").description("Show the Recordings.app diagnostic log").option("-n, --lines <lines>", "Number of lines to print", "120").action((opts) => {
6836
7518
  const status = getMacOSAppStatus();
6837
- if (!existsSync4(status.log_path)) {
7519
+ if (!existsSync5(status.log_path)) {
6838
7520
  console.log("");
6839
7521
  return;
6840
7522
  }
@@ -7322,13 +8004,13 @@ function getMacOSAppStatus() {
7322
8004
  platform: process.platform,
7323
8005
  package_root: packageRoot,
7324
8006
  installer_path: installerPath,
7325
- installer_available: existsSync4(installerPath),
8007
+ installer_available: existsSync5(installerPath),
7326
8008
  native_sources_path: nativeSourcesPath,
7327
- native_sources_available: existsSync4(pathJoin(nativeSourcesPath, "Package.swift")),
8009
+ native_sources_available: existsSync5(pathJoin(nativeSourcesPath, "Package.swift")),
7328
8010
  installed_app_path: installedAppPath,
7329
- installed: existsSync4(installedAppPath),
8011
+ installed: existsSync5(installedAppPath),
7330
8012
  executable_path: executablePath,
7331
- executable: existsSync4(executablePath),
8013
+ executable: existsSync5(executablePath),
7332
8014
  app_code_hash: signingInfo.cdHash,
7333
8015
  ad_hoc_signed: signingInfo.adHoc,
7334
8016
  microphone_permission: getTccPermission("kTCCServiceMicrophone", home, permissionCodeHash),
@@ -7349,7 +8031,7 @@ function resetMacOSPermissions() {
7349
8031
  }
7350
8032
  }
7351
8033
  function getCodeSigningInfo(appPath) {
7352
- if (process.platform !== "darwin" || !existsSync4(appPath)) {
8034
+ if (process.platform !== "darwin" || !existsSync5(appPath)) {
7353
8035
  return { cdHash: null, adHoc: false };
7354
8036
  }
7355
8037
  const result = spawnSync("codesign", ["-d", "--verbose=4", appPath], {
@@ -7371,7 +8053,7 @@ function getTccPermission(service, home, currentCodeHash) {
7371
8053
  ];
7372
8054
  const sql = "select auth_value || '|' || ifnull(hex(csreq), '') from access where service = '" + service.replace(/'/g, "''") + "' and client = 'com.hasna.recordings' order by last_modified desc limit 1;";
7373
8055
  for (const dbPath of dbPaths) {
7374
- if (!existsSync4(dbPath))
8056
+ if (!existsSync5(dbPath))
7375
8057
  continue;
7376
8058
  const result = spawnSync("sqlite3", [dbPath, sql], {
7377
8059
  encoding: "utf8",
@@ -7407,7 +8089,7 @@ function findPackageRoot() {
7407
8089
  let current = dirname2(fileURLToPath(import.meta.url));
7408
8090
  while (true) {
7409
8091
  const packagePath = pathJoin(current, "package.json");
7410
- if (existsSync4(packagePath)) {
8092
+ if (existsSync5(packagePath)) {
7411
8093
  try {
7412
8094
  const pkg = JSON.parse(readFileSync3(packagePath, "utf8"));
7413
8095
  if (pkg.name === "@hasna/recordings") {
package/dist/mcp/index.js CHANGED
@@ -4920,7 +4920,7 @@ var require_lib2 = __commonJS((exports, module) => {
4920
4920
  var require_package = __commonJS((exports, module) => {
4921
4921
  module.exports = {
4922
4922
  name: "@hasna/recordings",
4923
- version: "0.1.30",
4923
+ version: "0.1.31",
4924
4924
  type: "module",
4925
4925
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
4926
4926
  repository: {
@@ -4970,6 +4970,7 @@ var require_package = __commonJS((exports, module) => {
4970
4970
  "LICENSE"
4971
4971
  ],
4972
4972
  dependencies: {
4973
+ "@hasna/events": "^0.1.3",
4973
4974
  "@modelcontextprotocol/sdk": "^1.12.1",
4974
4975
  chalk: "^5.4.1",
4975
4976
  commander: "^13.1.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/recordings",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "description": "Speech-to-text recording tool with MCP and CLI — records, transcribes, and optionally enhances text using AI",
6
6
  "repository": {
@@ -50,6 +50,7 @@
50
50
  "LICENSE"
51
51
  ],
52
52
  "dependencies": {
53
+ "@hasna/events": "^0.1.3",
53
54
  "@modelcontextprotocol/sdk": "^1.12.1",
54
55
  "chalk": "^5.4.1",
55
56
  "commander": "^13.1.0",