@hasna/recordings 0.1.30 → 0.1.32
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 +732 -50
- package/dist/mcp/index.js +3 -2
- package/dist/version.d.ts +1 -1
- package/package.json +2 -1
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.
|
|
5101
|
+
version: "0.1.32",
|
|
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
|
|
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() ||
|
|
5215
|
-
if (
|
|
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 =
|
|
5939
|
+
config.db_path = join2(getDataDir(), "recordings.db");
|
|
5259
5940
|
}
|
|
5260
5941
|
if (!config.audio_dir) {
|
|
5261
|
-
config.audio_dir =
|
|
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 =
|
|
5280
|
-
if (
|
|
5960
|
+
const candidate = join2(dir, ".recordings", "config.json");
|
|
5961
|
+
if (existsSync2(candidate))
|
|
5281
5962
|
return candidate;
|
|
5282
|
-
const parent =
|
|
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 =
|
|
5294
|
-
if (
|
|
5974
|
+
const candidate = join2(dir, ".recordings");
|
|
5975
|
+
if (existsSync2(candidate))
|
|
5295
5976
|
return candidate;
|
|
5296
|
-
const parent =
|
|
5977
|
+
const parent = join2(dir, "..");
|
|
5297
5978
|
if (parent === dir)
|
|
5298
5979
|
break;
|
|
5299
5980
|
dir = parent;
|
|
5300
5981
|
}
|
|
5301
|
-
const home =
|
|
5302
|
-
const newDir =
|
|
5303
|
-
const oldDir =
|
|
5304
|
-
if (!
|
|
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(
|
|
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 =
|
|
5314
|
-
if (!
|
|
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 =
|
|
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
|
|
5707
|
-
import { existsSync as
|
|
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 =
|
|
6462
|
+
const filepath = join3(config.audio_dir, filename);
|
|
5782
6463
|
const args = buildRecordArgs(filepath, config);
|
|
5783
|
-
_recordProcess =
|
|
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 =
|
|
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 && !
|
|
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
|
}
|
|
@@ -6081,16 +6762,16 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
6081
6762
|
}
|
|
6082
6763
|
|
|
6083
6764
|
// src/version.ts
|
|
6084
|
-
var VERSION = "0.1.
|
|
6765
|
+
var VERSION = "0.1.32";
|
|
6085
6766
|
|
|
6086
6767
|
// src/cli/storage.ts
|
|
6087
6768
|
import chalk from "chalk";
|
|
6088
6769
|
|
|
6089
6770
|
// src/db/storage-config.ts
|
|
6090
|
-
import { existsSync as
|
|
6091
|
-
import { homedir as
|
|
6092
|
-
import { join as
|
|
6093
|
-
var STORAGE_CONFIG_PATH =
|
|
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 (
|
|
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:
|
|
6729
|
-
const { join:
|
|
6730
|
-
const dir =
|
|
6731
|
-
const audioDir =
|
|
6732
|
-
const configFile =
|
|
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 (!
|
|
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 (!
|
|
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:
|
|
8007
|
+
installer_available: existsSync5(installerPath),
|
|
7326
8008
|
native_sources_path: nativeSourcesPath,
|
|
7327
|
-
native_sources_available:
|
|
8009
|
+
native_sources_available: existsSync5(pathJoin(nativeSourcesPath, "Package.swift")),
|
|
7328
8010
|
installed_app_path: installedAppPath,
|
|
7329
|
-
installed:
|
|
8011
|
+
installed: existsSync5(installedAppPath),
|
|
7330
8012
|
executable_path: executablePath,
|
|
7331
|
-
executable:
|
|
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" || !
|
|
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 (!
|
|
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 (
|
|
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.
|
|
4923
|
+
version: "0.1.32",
|
|
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",
|
|
@@ -9797,7 +9798,7 @@ async function processText(rawText, config, systemPrompt) {
|
|
|
9797
9798
|
}
|
|
9798
9799
|
|
|
9799
9800
|
// src/version.ts
|
|
9800
|
-
var VERSION = "0.1.
|
|
9801
|
+
var VERSION = "0.1.32";
|
|
9801
9802
|
|
|
9802
9803
|
// src/db/storage-config.ts
|
|
9803
9804
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.32";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/recordings",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
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",
|