@hasna/machines 0.0.32 → 0.0.33
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/README.md +14 -13
- package/dist/cli/index.js +132 -1318
- package/dist/commands/screen.d.ts +2 -1
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/index.js +32 -544
- package/dist/mcp/index.js +85 -601
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -13440,528 +13440,7 @@ function listPorts(machineId) {
|
|
|
13440
13440
|
// src/commands/runtime.ts
|
|
13441
13441
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
13442
13442
|
import { setTimeout as sleep } from "timers/promises";
|
|
13443
|
-
|
|
13444
|
-
// node_modules/@hasna/events/dist/index.js
|
|
13445
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
13446
|
-
import { existsSync as existsSync7 } from "fs";
|
|
13447
|
-
import { homedir as homedir4 } from "os";
|
|
13448
|
-
import { join as join6 } from "path";
|
|
13449
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
13450
|
-
import { randomUUID } from "crypto";
|
|
13451
|
-
import { spawn } from "child_process";
|
|
13452
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
13453
|
-
function getPathValue(input, path) {
|
|
13454
|
-
return path.split(".").reduce((value, part) => {
|
|
13455
|
-
if (value && typeof value === "object" && part in value) {
|
|
13456
|
-
return value[part];
|
|
13457
|
-
}
|
|
13458
|
-
return;
|
|
13459
|
-
}, input);
|
|
13460
|
-
}
|
|
13461
|
-
function wildcardToRegExp(pattern) {
|
|
13462
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
13463
|
-
return new RegExp(`^${escaped}$`);
|
|
13464
|
-
}
|
|
13465
|
-
function matchString(value, matcher) {
|
|
13466
|
-
if (matcher === undefined)
|
|
13467
|
-
return true;
|
|
13468
|
-
if (value === undefined)
|
|
13469
|
-
return false;
|
|
13470
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
13471
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
13472
|
-
}
|
|
13473
|
-
function matchRecord(input, matcher) {
|
|
13474
|
-
if (!matcher)
|
|
13475
|
-
return true;
|
|
13476
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
13477
|
-
const actual = getPathValue(input, path);
|
|
13478
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
13479
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
13480
|
-
}
|
|
13481
|
-
return actual === expected;
|
|
13482
|
-
});
|
|
13483
|
-
}
|
|
13484
|
-
function eventMatchesFilter(event, filter) {
|
|
13485
|
-
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);
|
|
13486
|
-
}
|
|
13487
|
-
function channelMatchesEvent(channel, event) {
|
|
13488
|
-
if (!channel.enabled)
|
|
13489
|
-
return false;
|
|
13490
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
13491
|
-
return true;
|
|
13492
|
-
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
13493
|
-
}
|
|
13494
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
13495
|
-
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
13496
|
-
function getEventsDataDir(override) {
|
|
13497
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join6(homedir4(), ".hasna", "events");
|
|
13498
|
-
}
|
|
13499
|
-
|
|
13500
|
-
class JsonEventsStore {
|
|
13501
|
-
dataDir;
|
|
13502
|
-
channelsPath;
|
|
13503
|
-
eventsPath;
|
|
13504
|
-
deliveriesPath;
|
|
13505
|
-
constructor(dataDir = getEventsDataDir()) {
|
|
13506
|
-
this.dataDir = dataDir;
|
|
13507
|
-
this.channelsPath = join6(dataDir, "channels.json");
|
|
13508
|
-
this.eventsPath = join6(dataDir, "events.json");
|
|
13509
|
-
this.deliveriesPath = join6(dataDir, "deliveries.json");
|
|
13510
|
-
}
|
|
13511
|
-
async init() {
|
|
13512
|
-
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
13513
|
-
await chmod(this.dataDir, 448).catch(() => {
|
|
13514
|
-
return;
|
|
13515
|
-
});
|
|
13516
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
13517
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
13518
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
13519
|
-
}
|
|
13520
|
-
async addChannel(channel) {
|
|
13521
|
-
await this.init();
|
|
13522
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
13523
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
13524
|
-
if (index >= 0) {
|
|
13525
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
13526
|
-
} else {
|
|
13527
|
-
channels.push(channel);
|
|
13528
|
-
}
|
|
13529
|
-
await this.writeJson(this.channelsPath, channels);
|
|
13530
|
-
return index >= 0 ? channels[index] : channel;
|
|
13531
|
-
}
|
|
13532
|
-
async listChannels() {
|
|
13533
|
-
await this.init();
|
|
13534
|
-
return this.readJson(this.channelsPath, []);
|
|
13535
|
-
}
|
|
13536
|
-
async getChannel(id) {
|
|
13537
|
-
const channels = await this.listChannels();
|
|
13538
|
-
return channels.find((channel) => channel.id === id);
|
|
13539
|
-
}
|
|
13540
|
-
async removeChannel(id) {
|
|
13541
|
-
await this.init();
|
|
13542
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
13543
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
13544
|
-
await this.writeJson(this.channelsPath, next);
|
|
13545
|
-
return next.length !== channels.length;
|
|
13546
|
-
}
|
|
13547
|
-
async appendEvent(event) {
|
|
13548
|
-
await this.init();
|
|
13549
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
13550
|
-
events.push(event);
|
|
13551
|
-
await this.writeJson(this.eventsPath, events);
|
|
13552
|
-
return event;
|
|
13553
|
-
}
|
|
13554
|
-
async listEvents() {
|
|
13555
|
-
await this.init();
|
|
13556
|
-
return this.readJson(this.eventsPath, []);
|
|
13557
|
-
}
|
|
13558
|
-
async findEventByIdentity(identity) {
|
|
13559
|
-
const events = await this.listEvents();
|
|
13560
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
13561
|
-
}
|
|
13562
|
-
async appendDelivery(result) {
|
|
13563
|
-
await this.init();
|
|
13564
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
13565
|
-
deliveries.push(result);
|
|
13566
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
13567
|
-
return result;
|
|
13568
|
-
}
|
|
13569
|
-
async listDeliveries() {
|
|
13570
|
-
await this.init();
|
|
13571
|
-
return this.readJson(this.deliveriesPath, []);
|
|
13572
|
-
}
|
|
13573
|
-
async exportData() {
|
|
13574
|
-
return {
|
|
13575
|
-
channels: await this.listChannels(),
|
|
13576
|
-
events: await this.listEvents(),
|
|
13577
|
-
deliveries: await this.listDeliveries()
|
|
13578
|
-
};
|
|
13579
|
-
}
|
|
13580
|
-
async ensureArrayFile(path) {
|
|
13581
|
-
if (!existsSync7(path)) {
|
|
13582
|
-
await writeFile(path, `[]
|
|
13583
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
13584
|
-
}
|
|
13585
|
-
await chmod(path, 384).catch(() => {
|
|
13586
|
-
return;
|
|
13587
|
-
});
|
|
13588
|
-
}
|
|
13589
|
-
async readJson(path, fallback) {
|
|
13590
|
-
try {
|
|
13591
|
-
const raw = await readFile(path, "utf-8");
|
|
13592
|
-
if (!raw.trim())
|
|
13593
|
-
return fallback;
|
|
13594
|
-
return JSON.parse(raw);
|
|
13595
|
-
} catch (error) {
|
|
13596
|
-
if (error.code === "ENOENT")
|
|
13597
|
-
return fallback;
|
|
13598
|
-
throw error;
|
|
13599
|
-
}
|
|
13600
|
-
}
|
|
13601
|
-
async writeJson(path, value) {
|
|
13602
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
13603
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
13604
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
13605
|
-
await rename(tempPath, path);
|
|
13606
|
-
await chmod(path, 384).catch(() => {
|
|
13607
|
-
return;
|
|
13608
|
-
});
|
|
13609
|
-
}
|
|
13610
|
-
}
|
|
13611
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
13612
|
-
function buildSignatureBase(timestamp, body) {
|
|
13613
|
-
return `${timestamp}.${body}`;
|
|
13614
|
-
}
|
|
13615
|
-
function signPayload(secret, timestamp, body) {
|
|
13616
|
-
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
13617
|
-
return `sha256=${digest}`;
|
|
13618
|
-
}
|
|
13619
|
-
function now() {
|
|
13620
|
-
return new Date().toISOString();
|
|
13621
|
-
}
|
|
13622
|
-
function truncate(value, max = 4096) {
|
|
13623
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
13624
|
-
}
|
|
13625
|
-
function buildWebhookRequest(event, channel) {
|
|
13626
|
-
if (!channel.webhook)
|
|
13627
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13628
|
-
const body = JSON.stringify(event);
|
|
13629
|
-
const timestamp = event.time;
|
|
13630
|
-
const headers = {
|
|
13631
|
-
"Content-Type": "application/json",
|
|
13632
|
-
"User-Agent": "@hasna/events",
|
|
13633
|
-
"X-Hasna-Event-Id": event.id,
|
|
13634
|
-
"X-Hasna-Event-Type": event.type,
|
|
13635
|
-
"X-Hasna-Timestamp": timestamp,
|
|
13636
|
-
...channel.webhook.headers
|
|
13637
|
-
};
|
|
13638
|
-
if (channel.webhook.secret) {
|
|
13639
|
-
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
13640
|
-
}
|
|
13641
|
-
return { body, headers };
|
|
13642
|
-
}
|
|
13643
|
-
async function dispatchWebhook2(event, channel, options = {}) {
|
|
13644
|
-
if (!channel.webhook)
|
|
13645
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13646
|
-
const startedAt = now();
|
|
13647
|
-
const { body, headers } = buildWebhookRequest(event, channel);
|
|
13648
|
-
const controller = new AbortController;
|
|
13649
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
13650
|
-
try {
|
|
13651
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
13652
|
-
method: "POST",
|
|
13653
|
-
headers,
|
|
13654
|
-
body,
|
|
13655
|
-
signal: controller.signal
|
|
13656
|
-
});
|
|
13657
|
-
const responseBody = truncate(await response.text());
|
|
13658
|
-
return {
|
|
13659
|
-
attempt: 1,
|
|
13660
|
-
status: response.ok ? "success" : "failed",
|
|
13661
|
-
startedAt,
|
|
13662
|
-
completedAt: now(),
|
|
13663
|
-
responseStatus: response.status,
|
|
13664
|
-
responseBody,
|
|
13665
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
13666
|
-
};
|
|
13667
|
-
} catch (error) {
|
|
13668
|
-
return {
|
|
13669
|
-
attempt: 1,
|
|
13670
|
-
status: "failed",
|
|
13671
|
-
startedAt,
|
|
13672
|
-
completedAt: now(),
|
|
13673
|
-
error: error instanceof Error ? error.message : String(error)
|
|
13674
|
-
};
|
|
13675
|
-
} finally {
|
|
13676
|
-
clearTimeout(timeout);
|
|
13677
|
-
}
|
|
13678
|
-
}
|
|
13679
|
-
async function dispatchCommand2(event, channel) {
|
|
13680
|
-
if (!channel.command)
|
|
13681
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
13682
|
-
const startedAt = now();
|
|
13683
|
-
const eventJson = JSON.stringify(event);
|
|
13684
|
-
const env = {
|
|
13685
|
-
...process.env,
|
|
13686
|
-
...channel.command.env,
|
|
13687
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
13688
|
-
HASNA_EVENT_ID: event.id,
|
|
13689
|
-
HASNA_EVENT_TYPE: event.type,
|
|
13690
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
13691
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
13692
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
13693
|
-
HASNA_EVENT_TIME: event.time,
|
|
13694
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
13695
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
13696
|
-
HASNA_EVENT_JSON: eventJson
|
|
13697
|
-
};
|
|
13698
|
-
return new Promise((resolve2) => {
|
|
13699
|
-
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
13700
|
-
cwd: channel.command.cwd,
|
|
13701
|
-
env,
|
|
13702
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
13703
|
-
});
|
|
13704
|
-
let stdout = "";
|
|
13705
|
-
let stderr = "";
|
|
13706
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
13707
|
-
child.stdin.end(eventJson);
|
|
13708
|
-
child.stdout.on("data", (chunk) => {
|
|
13709
|
-
stdout += chunk.toString();
|
|
13710
|
-
});
|
|
13711
|
-
child.stderr.on("data", (chunk) => {
|
|
13712
|
-
stderr += chunk.toString();
|
|
13713
|
-
});
|
|
13714
|
-
child.on("error", (error) => {
|
|
13715
|
-
clearTimeout(timeout);
|
|
13716
|
-
resolve2({
|
|
13717
|
-
attempt: 1,
|
|
13718
|
-
status: "failed",
|
|
13719
|
-
startedAt,
|
|
13720
|
-
completedAt: now(),
|
|
13721
|
-
stdout: truncate(stdout),
|
|
13722
|
-
stderr: truncate(stderr),
|
|
13723
|
-
error: error.message
|
|
13724
|
-
});
|
|
13725
|
-
});
|
|
13726
|
-
child.on("close", (code, signal) => {
|
|
13727
|
-
clearTimeout(timeout);
|
|
13728
|
-
const success = code === 0;
|
|
13729
|
-
resolve2({
|
|
13730
|
-
attempt: 1,
|
|
13731
|
-
status: success ? "success" : "failed",
|
|
13732
|
-
startedAt,
|
|
13733
|
-
completedAt: now(),
|
|
13734
|
-
stdout: truncate(stdout),
|
|
13735
|
-
stderr: truncate(stderr),
|
|
13736
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
13737
|
-
});
|
|
13738
|
-
});
|
|
13739
|
-
});
|
|
13740
|
-
}
|
|
13741
|
-
async function dispatchChannel2(event, channel, options = {}) {
|
|
13742
|
-
if (channel.transport === "webhook")
|
|
13743
|
-
return dispatchWebhook2(event, channel, options);
|
|
13744
|
-
if (channel.transport === "command")
|
|
13745
|
-
return dispatchCommand2(event, channel);
|
|
13746
|
-
return {
|
|
13747
|
-
attempt: 1,
|
|
13748
|
-
status: "skipped",
|
|
13749
|
-
startedAt: now(),
|
|
13750
|
-
completedAt: now(),
|
|
13751
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
13752
|
-
};
|
|
13753
|
-
}
|
|
13754
|
-
function createDeliveryResult(event, channel, attempts) {
|
|
13755
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
13756
|
-
return {
|
|
13757
|
-
id: randomUUID(),
|
|
13758
|
-
eventId: event.id,
|
|
13759
|
-
channelId: channel.id,
|
|
13760
|
-
transport: channel.transport,
|
|
13761
|
-
status,
|
|
13762
|
-
attempts,
|
|
13763
|
-
createdAt: attempts[0]?.startedAt ?? now(),
|
|
13764
|
-
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
13765
|
-
};
|
|
13766
|
-
}
|
|
13767
|
-
function createEvent(input) {
|
|
13768
|
-
return {
|
|
13769
|
-
id: input.id ?? randomUUID2(),
|
|
13770
|
-
source: input.source,
|
|
13771
|
-
type: input.type,
|
|
13772
|
-
time: normalizeTime(input.time),
|
|
13773
|
-
subject: input.subject,
|
|
13774
|
-
severity: input.severity ?? "info",
|
|
13775
|
-
data: input.data ?? {},
|
|
13776
|
-
message: input.message,
|
|
13777
|
-
dedupeKey: input.dedupeKey,
|
|
13778
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
13779
|
-
metadata: input.metadata ?? {}
|
|
13780
|
-
};
|
|
13781
|
-
}
|
|
13782
|
-
|
|
13783
|
-
class EventsClient {
|
|
13784
|
-
store;
|
|
13785
|
-
redactors;
|
|
13786
|
-
transportOptions;
|
|
13787
|
-
constructor(options = {}) {
|
|
13788
|
-
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
13789
|
-
this.redactors = options.redactors ?? [];
|
|
13790
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
13791
|
-
}
|
|
13792
|
-
async addChannel(input) {
|
|
13793
|
-
const timestamp = new Date().toISOString();
|
|
13794
|
-
return this.store.addChannel({
|
|
13795
|
-
...input,
|
|
13796
|
-
createdAt: input.createdAt ?? timestamp,
|
|
13797
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
13798
|
-
});
|
|
13799
|
-
}
|
|
13800
|
-
async listChannels() {
|
|
13801
|
-
return this.store.listChannels();
|
|
13802
|
-
}
|
|
13803
|
-
async removeChannel(id) {
|
|
13804
|
-
return this.store.removeChannel(id);
|
|
13805
|
-
}
|
|
13806
|
-
async emit(input, options = {}) {
|
|
13807
|
-
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
13808
|
-
if (options.dedupe !== false) {
|
|
13809
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
13810
|
-
if (existing) {
|
|
13811
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
13812
|
-
}
|
|
13813
|
-
}
|
|
13814
|
-
await this.store.appendEvent(event);
|
|
13815
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
13816
|
-
return { event, deliveries, deduped: false };
|
|
13817
|
-
}
|
|
13818
|
-
async listEvents() {
|
|
13819
|
-
return this.store.listEvents();
|
|
13820
|
-
}
|
|
13821
|
-
async listDeliveries() {
|
|
13822
|
-
return this.store.listDeliveries();
|
|
13823
|
-
}
|
|
13824
|
-
async deliver(event) {
|
|
13825
|
-
const channels = await this.store.listChannels();
|
|
13826
|
-
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
13827
|
-
const deliveries = [];
|
|
13828
|
-
for (const channel of selected) {
|
|
13829
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13830
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13831
|
-
await this.store.appendDelivery(result);
|
|
13832
|
-
deliveries.push(result);
|
|
13833
|
-
}
|
|
13834
|
-
return deliveries;
|
|
13835
|
-
}
|
|
13836
|
-
async testChannel(id, input = {}) {
|
|
13837
|
-
const channel = await this.store.getChannel(id);
|
|
13838
|
-
if (!channel)
|
|
13839
|
-
throw new Error(`Channel not found: ${id}`);
|
|
13840
|
-
const event = createEvent({
|
|
13841
|
-
source: input.source ?? "hasna.events",
|
|
13842
|
-
type: input.type ?? "events.test",
|
|
13843
|
-
subject: input.subject ?? id,
|
|
13844
|
-
severity: input.severity ?? "info",
|
|
13845
|
-
data: input.data ?? { test: true },
|
|
13846
|
-
message: input.message ?? "Hasna events test delivery",
|
|
13847
|
-
dedupeKey: input.dedupeKey,
|
|
13848
|
-
schemaVersion: input.schemaVersion,
|
|
13849
|
-
metadata: input.metadata,
|
|
13850
|
-
time: input.time,
|
|
13851
|
-
id: input.id
|
|
13852
|
-
});
|
|
13853
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13854
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13855
|
-
await this.store.appendDelivery(result);
|
|
13856
|
-
return result;
|
|
13857
|
-
}
|
|
13858
|
-
async replay(options = {}) {
|
|
13859
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
13860
|
-
if (options.eventId && event.id !== options.eventId)
|
|
13861
|
-
return false;
|
|
13862
|
-
if (options.source && event.source !== options.source)
|
|
13863
|
-
return false;
|
|
13864
|
-
if (options.type && event.type !== options.type)
|
|
13865
|
-
return false;
|
|
13866
|
-
return true;
|
|
13867
|
-
});
|
|
13868
|
-
if (options.dryRun)
|
|
13869
|
-
return { events, deliveries: [] };
|
|
13870
|
-
const deliveries = [];
|
|
13871
|
-
for (const event of events) {
|
|
13872
|
-
deliveries.push(...await this.deliver(event));
|
|
13873
|
-
}
|
|
13874
|
-
return { events, deliveries };
|
|
13875
|
-
}
|
|
13876
|
-
async applyRedaction(event, channel) {
|
|
13877
|
-
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
13878
|
-
for (const redactor of this.redactors) {
|
|
13879
|
-
next = await redactor(next, channel);
|
|
13880
|
-
}
|
|
13881
|
-
return next;
|
|
13882
|
-
}
|
|
13883
|
-
async deliverWithRetry(event, channel) {
|
|
13884
|
-
const policy = normalizeRetryPolicy(channel.retry);
|
|
13885
|
-
const attempts = [];
|
|
13886
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
13887
|
-
const attempt = await dispatchChannel2(event, channel, this.transportOptions);
|
|
13888
|
-
attempt.attempt = index + 1;
|
|
13889
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
13890
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
13891
|
-
}
|
|
13892
|
-
attempts.push(attempt);
|
|
13893
|
-
if (attempt.status !== "failed")
|
|
13894
|
-
break;
|
|
13895
|
-
if (attempt.nextBackoffMs)
|
|
13896
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
13897
|
-
}
|
|
13898
|
-
return createDeliveryResult(event, channel, attempts);
|
|
13899
|
-
}
|
|
13900
|
-
}
|
|
13901
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
13902
|
-
if (paths.length === 0)
|
|
13903
|
-
return event;
|
|
13904
|
-
const copy = structuredClone(event);
|
|
13905
|
-
for (const path of paths) {
|
|
13906
|
-
setPath(copy, path, replacement);
|
|
13907
|
-
}
|
|
13908
|
-
return copy;
|
|
13909
|
-
}
|
|
13910
|
-
function sanitizeChannelForOutput(channel) {
|
|
13911
|
-
const copy = structuredClone(channel);
|
|
13912
|
-
if (copy.webhook?.secret)
|
|
13913
|
-
copy.webhook.secret = "[REDACTED]";
|
|
13914
|
-
if (copy.command?.env) {
|
|
13915
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
13916
|
-
}
|
|
13917
|
-
return copy;
|
|
13918
|
-
}
|
|
13919
|
-
function sanitizeChannelsForOutput(channels) {
|
|
13920
|
-
return channels.map(sanitizeChannelForOutput);
|
|
13921
|
-
}
|
|
13922
|
-
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
13923
|
-
return redactValue(event, replacement);
|
|
13924
|
-
}
|
|
13925
|
-
function shouldRedactKey(key) {
|
|
13926
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
13927
|
-
}
|
|
13928
|
-
function redactValue(value, replacement) {
|
|
13929
|
-
if (Array.isArray(value))
|
|
13930
|
-
return value.map((item) => redactValue(item, replacement));
|
|
13931
|
-
if (!value || typeof value !== "object")
|
|
13932
|
-
return value;
|
|
13933
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
13934
|
-
key,
|
|
13935
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
13936
|
-
]));
|
|
13937
|
-
}
|
|
13938
|
-
function setPath(input, path, replacement) {
|
|
13939
|
-
const parts = path.split(".");
|
|
13940
|
-
let cursor = input;
|
|
13941
|
-
for (const part of parts.slice(0, -1)) {
|
|
13942
|
-
const next = cursor[part];
|
|
13943
|
-
if (!next || typeof next !== "object")
|
|
13944
|
-
return;
|
|
13945
|
-
cursor = next;
|
|
13946
|
-
}
|
|
13947
|
-
const last = parts.at(-1);
|
|
13948
|
-
if (last && last in cursor)
|
|
13949
|
-
cursor[last] = replacement;
|
|
13950
|
-
}
|
|
13951
|
-
function normalizeTime(value) {
|
|
13952
|
-
if (!value)
|
|
13953
|
-
return new Date().toISOString();
|
|
13954
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
13955
|
-
}
|
|
13956
|
-
function normalizeRetryPolicy(policy) {
|
|
13957
|
-
return {
|
|
13958
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
13959
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
13960
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
13961
|
-
};
|
|
13962
|
-
}
|
|
13963
|
-
|
|
13964
|
-
// src/commands/runtime.ts
|
|
13443
|
+
import { EventsClient } from "@hasna/events";
|
|
13965
13444
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
13966
13445
|
const checkedAt = new Date().toISOString();
|
|
13967
13446
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -14046,6 +13525,9 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
14046
13525
|
};
|
|
14047
13526
|
return client.emit(input, { deliver });
|
|
14048
13527
|
}
|
|
13528
|
+
// src/commands/serve.ts
|
|
13529
|
+
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
13530
|
+
|
|
14049
13531
|
// src/commands/status.ts
|
|
14050
13532
|
function getStatus() {
|
|
14051
13533
|
const manifest = readManifest();
|
|
@@ -14271,7 +13753,7 @@ function jsonError(message, status = 400) {
|
|
|
14271
13753
|
}
|
|
14272
13754
|
function startDashboardServer(options = {}) {
|
|
14273
13755
|
const info = getServeInfo(options);
|
|
14274
|
-
const events = new
|
|
13756
|
+
const events = new EventsClient2;
|
|
14275
13757
|
return Bun.serve({
|
|
14276
13758
|
hostname: info.host,
|
|
14277
13759
|
port: info.port,
|
|
@@ -14425,7 +13907,7 @@ function runSelfTest() {
|
|
|
14425
13907
|
};
|
|
14426
13908
|
}
|
|
14427
13909
|
// src/commands/setup.ts
|
|
14428
|
-
import { homedir as
|
|
13910
|
+
import { homedir as homedir4 } from "os";
|
|
14429
13911
|
function quote3(value) {
|
|
14430
13912
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14431
13913
|
}
|
|
@@ -14498,7 +13980,7 @@ function buildSetupPlan(machineId) {
|
|
|
14498
13980
|
const target = selected || {
|
|
14499
13981
|
id: currentMachineId,
|
|
14500
13982
|
platform: "linux",
|
|
14501
|
-
workspacePath: `${
|
|
13983
|
+
workspacePath: `${homedir4()}/workspace`
|
|
14502
13984
|
};
|
|
14503
13985
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
14504
13986
|
return {
|
|
@@ -14542,7 +14024,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
14542
14024
|
return summary;
|
|
14543
14025
|
}
|
|
14544
14026
|
// src/commands/screen.ts
|
|
14545
|
-
var
|
|
14027
|
+
var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
|
|
14028
|
+
var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
|
|
14546
14029
|
function shellQuote7(value) {
|
|
14547
14030
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
14548
14031
|
}
|
|
@@ -14566,7 +14049,8 @@ function splitTarget(target) {
|
|
|
14566
14049
|
return [target.slice(0, at), target.slice(at + 1)];
|
|
14567
14050
|
}
|
|
14568
14051
|
function defaultScreenPasswordSecretKey(machineId) {
|
|
14569
|
-
|
|
14052
|
+
const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
|
|
14053
|
+
return `${namespace}/screen-${machineId}-vnc-password`;
|
|
14570
14054
|
}
|
|
14571
14055
|
function resolveScreenTarget(machineId, options = {}) {
|
|
14572
14056
|
const resolved = resolveMachineRoute(machineId, options);
|
|
@@ -14663,8 +14147,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
14663
14147
|
};
|
|
14664
14148
|
}
|
|
14665
14149
|
// src/commands/sync.ts
|
|
14666
|
-
import { existsSync as
|
|
14667
|
-
import { homedir as
|
|
14150
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
14151
|
+
import { homedir as homedir5 } from "os";
|
|
14668
14152
|
function quote4(value) {
|
|
14669
14153
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14670
14154
|
}
|
|
@@ -14716,8 +14200,8 @@ function detectFileActions(machine) {
|
|
|
14716
14200
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
14717
14201
|
}
|
|
14718
14202
|
return (machine.files || []).map((file, index) => {
|
|
14719
|
-
const sourceExists =
|
|
14720
|
-
const targetExists =
|
|
14203
|
+
const sourceExists = existsSync7(file.source);
|
|
14204
|
+
const targetExists = existsSync7(file.target);
|
|
14721
14205
|
let status = "missing";
|
|
14722
14206
|
if (sourceExists && targetExists) {
|
|
14723
14207
|
if (file.mode === "symlink") {
|
|
@@ -14748,7 +14232,7 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
14748
14232
|
const target = selected || {
|
|
14749
14233
|
id: currentMachineId,
|
|
14750
14234
|
platform: "linux",
|
|
14751
|
-
workspacePath: `${
|
|
14235
|
+
workspacePath: `${homedir5()}/workspace`
|
|
14752
14236
|
};
|
|
14753
14237
|
const actions = [
|
|
14754
14238
|
...detectPackageActions(target, runner),
|
|
@@ -14968,6 +14452,9 @@ function repairWorkspaceManifestMappings(options) {
|
|
|
14968
14452
|
warnings
|
|
14969
14453
|
};
|
|
14970
14454
|
}
|
|
14455
|
+
// src/mcp/server.ts
|
|
14456
|
+
import { EventsClient as EventsClient3, sanitizeChannelForOutput, sanitizeChannelsForOutput as sanitizeChannelsForOutput2 } from "@hasna/events";
|
|
14457
|
+
|
|
14971
14458
|
// node_modules/zod/v4/core/core.js
|
|
14972
14459
|
var NEVER2 = Object.freeze({
|
|
14973
14460
|
status: "aborted"
|
|
@@ -23940,7 +23427,7 @@ function buildServer(version2 = getPackageVersion()) {
|
|
|
23940
23427
|
}
|
|
23941
23428
|
function createMcpServer(version2) {
|
|
23942
23429
|
const server = new McpServer({ name: "machines", version: version2 });
|
|
23943
|
-
const events = new
|
|
23430
|
+
const events = new EventsClient3;
|
|
23944
23431
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
23945
23432
|
content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
|
|
23946
23433
|
}));
|
|
@@ -24094,7 +23581,7 @@ function createMcpServer(version2) {
|
|
|
24094
23581
|
}));
|
|
24095
23582
|
server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external.string().describe("Event name"), message: exports_external.string().describe("Message body"), channel_id: exports_external.string().optional().describe("Limit delivery to one channel") }, async ({ event, message, channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id }), null, 2) }] }));
|
|
24096
23583
|
server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] }));
|
|
24097
|
-
server.tool("machines_webhooks_add", "Add or replace a shared
|
|
23584
|
+
server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
|
|
24098
23585
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
24099
23586
|
url: exports_external.string().url().describe("Webhook URL"),
|
|
24100
23587
|
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
@@ -24102,26 +23589,26 @@ function createMcpServer(version2) {
|
|
|
24102
23589
|
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
24103
23590
|
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
24104
23591
|
}, async ({ channel_id, url, event_type, source, secret, enabled }) => {
|
|
24105
|
-
const
|
|
23592
|
+
const now = new Date().toISOString();
|
|
24106
23593
|
const channel = await events.addChannel({
|
|
24107
23594
|
id: channel_id,
|
|
24108
23595
|
enabled: enabled ?? true,
|
|
24109
23596
|
transport: "webhook",
|
|
24110
23597
|
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
24111
23598
|
webhook: { url, secret },
|
|
24112
|
-
createdAt:
|
|
24113
|
-
updatedAt:
|
|
23599
|
+
createdAt: now,
|
|
23600
|
+
updatedAt: now
|
|
24114
23601
|
});
|
|
24115
23602
|
return { content: [{ type: "text", text: JSON.stringify(sanitizeChannelForOutput(channel), null, 2) }] };
|
|
24116
23603
|
});
|
|
24117
|
-
server.tool("machines_webhooks_list", "List shared
|
|
24118
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
23604
|
+
server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
|
|
23605
|
+
content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
|
|
24119
23606
|
}));
|
|
24120
|
-
server.tool("machines_webhooks_test", "Send a test event to one shared
|
|
23607
|
+
server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body") }, async ({ channel_id, event_type, message }) => ({
|
|
24121
23608
|
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
24122
23609
|
}));
|
|
24123
|
-
server.tool("machines_webhooks_remove", "Remove a shared
|
|
24124
|
-
server.tool("machines_events_emit", "Emit a shared
|
|
23610
|
+
server.tool("machines_webhooks_remove", "Remove a shared event channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify({ removed: await events.removeChannel(channel_id) }, null, 2) }] }));
|
|
23611
|
+
server.tool("machines_events_emit", "Emit a shared event from machines.", {
|
|
24125
23612
|
event_type: exports_external.string().describe("Event type"),
|
|
24126
23613
|
subject: exports_external.string().optional().describe("Event subject"),
|
|
24127
23614
|
severity: exports_external.enum(["debug", "info", "notice", "warning", "error", "critical"]).optional().describe("Event severity"),
|
|
@@ -24142,10 +23629,10 @@ function createMcpServer(version2) {
|
|
|
24142
23629
|
dedupeKey: dedupe_key
|
|
24143
23630
|
}, { deliver: deliver !== false }), null, 2) }]
|
|
24144
23631
|
}));
|
|
24145
|
-
server.tool("machines_events_list", "List shared
|
|
23632
|
+
server.tool("machines_events_list", "List shared events.", {}, async () => ({
|
|
24146
23633
|
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
24147
23634
|
}));
|
|
24148
|
-
server.tool("machines_events_replay", "Replay shared
|
|
23635
|
+
server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery") }, async ({ event_id, source, event_type, dry_run }) => ({
|
|
24149
23636
|
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
24150
23637
|
}));
|
|
24151
23638
|
server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", { host: exports_external.string().optional().describe("Host interface"), port: exports_external.number().optional().describe("Port number") }, async ({ host, port }) => ({ content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }] }));
|
|
@@ -24276,6 +23763,7 @@ export {
|
|
|
24276
23763
|
STORAGE_TABLES,
|
|
24277
23764
|
STORAGE_MODE_ENV,
|
|
24278
23765
|
STORAGE_DATABASE_ENV,
|
|
23766
|
+
SCREEN_SECRET_NAMESPACE_ENV,
|
|
24279
23767
|
MACHINE_MCP_TOOL_NAMES,
|
|
24280
23768
|
MACHINES_STORAGE_TABLES,
|
|
24281
23769
|
MACHINES_STORAGE_MODE_FALLBACK_ENV,
|