@hasna/machines 0.0.26 → 0.0.28
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 +12 -3
- package/dist/cli/index.js +1502 -131
- package/dist/commands/screen.d.ts +31 -0
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/index.js +731 -10
- package/dist/mcp/index.js +650 -72
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -13427,6 +13427,526 @@ function listPorts(machineId) {
|
|
|
13427
13427
|
listeners: parsePortOutput(result.stdout, format)
|
|
13428
13428
|
};
|
|
13429
13429
|
}
|
|
13430
|
+
// node_modules/@hasna/events/dist/index.js
|
|
13431
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
13432
|
+
import { existsSync as existsSync7 } from "fs";
|
|
13433
|
+
import { homedir as homedir4 } from "os";
|
|
13434
|
+
import { join as join6 } from "path";
|
|
13435
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
13436
|
+
import { randomUUID } from "crypto";
|
|
13437
|
+
import { spawn } from "child_process";
|
|
13438
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
13439
|
+
function getPathValue(input, path) {
|
|
13440
|
+
return path.split(".").reduce((value, part) => {
|
|
13441
|
+
if (value && typeof value === "object" && part in value) {
|
|
13442
|
+
return value[part];
|
|
13443
|
+
}
|
|
13444
|
+
return;
|
|
13445
|
+
}, input);
|
|
13446
|
+
}
|
|
13447
|
+
function wildcardToRegExp(pattern) {
|
|
13448
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
13449
|
+
return new RegExp(`^${escaped}$`);
|
|
13450
|
+
}
|
|
13451
|
+
function matchString(value, matcher) {
|
|
13452
|
+
if (matcher === undefined)
|
|
13453
|
+
return true;
|
|
13454
|
+
if (value === undefined)
|
|
13455
|
+
return false;
|
|
13456
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
13457
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
13458
|
+
}
|
|
13459
|
+
function matchRecord(input, matcher) {
|
|
13460
|
+
if (!matcher)
|
|
13461
|
+
return true;
|
|
13462
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
13463
|
+
const actual = getPathValue(input, path);
|
|
13464
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
13465
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
13466
|
+
}
|
|
13467
|
+
return actual === expected;
|
|
13468
|
+
});
|
|
13469
|
+
}
|
|
13470
|
+
function eventMatchesFilter(event, filter) {
|
|
13471
|
+
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);
|
|
13472
|
+
}
|
|
13473
|
+
function channelMatchesEvent(channel, event) {
|
|
13474
|
+
if (!channel.enabled)
|
|
13475
|
+
return false;
|
|
13476
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
13477
|
+
return true;
|
|
13478
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
13479
|
+
}
|
|
13480
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
13481
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
13482
|
+
function getEventsDataDir(override) {
|
|
13483
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join6(homedir4(), ".hasna", "events");
|
|
13484
|
+
}
|
|
13485
|
+
|
|
13486
|
+
class JsonEventsStore {
|
|
13487
|
+
dataDir;
|
|
13488
|
+
channelsPath;
|
|
13489
|
+
eventsPath;
|
|
13490
|
+
deliveriesPath;
|
|
13491
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
13492
|
+
this.dataDir = dataDir;
|
|
13493
|
+
this.channelsPath = join6(dataDir, "channels.json");
|
|
13494
|
+
this.eventsPath = join6(dataDir, "events.json");
|
|
13495
|
+
this.deliveriesPath = join6(dataDir, "deliveries.json");
|
|
13496
|
+
}
|
|
13497
|
+
async init() {
|
|
13498
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
13499
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
13500
|
+
return;
|
|
13501
|
+
});
|
|
13502
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
13503
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
13504
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
13505
|
+
}
|
|
13506
|
+
async addChannel(channel) {
|
|
13507
|
+
await this.init();
|
|
13508
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
13509
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
13510
|
+
if (index >= 0) {
|
|
13511
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
13512
|
+
} else {
|
|
13513
|
+
channels.push(channel);
|
|
13514
|
+
}
|
|
13515
|
+
await this.writeJson(this.channelsPath, channels);
|
|
13516
|
+
return index >= 0 ? channels[index] : channel;
|
|
13517
|
+
}
|
|
13518
|
+
async listChannels() {
|
|
13519
|
+
await this.init();
|
|
13520
|
+
return this.readJson(this.channelsPath, []);
|
|
13521
|
+
}
|
|
13522
|
+
async getChannel(id) {
|
|
13523
|
+
const channels = await this.listChannels();
|
|
13524
|
+
return channels.find((channel) => channel.id === id);
|
|
13525
|
+
}
|
|
13526
|
+
async removeChannel(id) {
|
|
13527
|
+
await this.init();
|
|
13528
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
13529
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
13530
|
+
await this.writeJson(this.channelsPath, next);
|
|
13531
|
+
return next.length !== channels.length;
|
|
13532
|
+
}
|
|
13533
|
+
async appendEvent(event) {
|
|
13534
|
+
await this.init();
|
|
13535
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
13536
|
+
events.push(event);
|
|
13537
|
+
await this.writeJson(this.eventsPath, events);
|
|
13538
|
+
return event;
|
|
13539
|
+
}
|
|
13540
|
+
async listEvents() {
|
|
13541
|
+
await this.init();
|
|
13542
|
+
return this.readJson(this.eventsPath, []);
|
|
13543
|
+
}
|
|
13544
|
+
async findEventByIdentity(identity) {
|
|
13545
|
+
const events = await this.listEvents();
|
|
13546
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
13547
|
+
}
|
|
13548
|
+
async appendDelivery(result) {
|
|
13549
|
+
await this.init();
|
|
13550
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
13551
|
+
deliveries.push(result);
|
|
13552
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
13553
|
+
return result;
|
|
13554
|
+
}
|
|
13555
|
+
async listDeliveries() {
|
|
13556
|
+
await this.init();
|
|
13557
|
+
return this.readJson(this.deliveriesPath, []);
|
|
13558
|
+
}
|
|
13559
|
+
async exportData() {
|
|
13560
|
+
return {
|
|
13561
|
+
channels: await this.listChannels(),
|
|
13562
|
+
events: await this.listEvents(),
|
|
13563
|
+
deliveries: await this.listDeliveries()
|
|
13564
|
+
};
|
|
13565
|
+
}
|
|
13566
|
+
async ensureArrayFile(path) {
|
|
13567
|
+
if (!existsSync7(path)) {
|
|
13568
|
+
await writeFile(path, `[]
|
|
13569
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
13570
|
+
}
|
|
13571
|
+
await chmod(path, 384).catch(() => {
|
|
13572
|
+
return;
|
|
13573
|
+
});
|
|
13574
|
+
}
|
|
13575
|
+
async readJson(path, fallback) {
|
|
13576
|
+
try {
|
|
13577
|
+
const raw = await readFile(path, "utf-8");
|
|
13578
|
+
if (!raw.trim())
|
|
13579
|
+
return fallback;
|
|
13580
|
+
return JSON.parse(raw);
|
|
13581
|
+
} catch (error) {
|
|
13582
|
+
if (error.code === "ENOENT")
|
|
13583
|
+
return fallback;
|
|
13584
|
+
throw error;
|
|
13585
|
+
}
|
|
13586
|
+
}
|
|
13587
|
+
async writeJson(path, value) {
|
|
13588
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
13589
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
13590
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
13591
|
+
await rename(tempPath, path);
|
|
13592
|
+
await chmod(path, 384).catch(() => {
|
|
13593
|
+
return;
|
|
13594
|
+
});
|
|
13595
|
+
}
|
|
13596
|
+
}
|
|
13597
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
13598
|
+
function buildSignatureBase(timestamp, body) {
|
|
13599
|
+
return `${timestamp}.${body}`;
|
|
13600
|
+
}
|
|
13601
|
+
function signPayload(secret, timestamp, body) {
|
|
13602
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
13603
|
+
return `sha256=${digest}`;
|
|
13604
|
+
}
|
|
13605
|
+
function now() {
|
|
13606
|
+
return new Date().toISOString();
|
|
13607
|
+
}
|
|
13608
|
+
function truncate(value, max = 4096) {
|
|
13609
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
13610
|
+
}
|
|
13611
|
+
function buildWebhookRequest(event, channel) {
|
|
13612
|
+
if (!channel.webhook)
|
|
13613
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13614
|
+
const body = JSON.stringify(event);
|
|
13615
|
+
const timestamp = event.time;
|
|
13616
|
+
const headers = {
|
|
13617
|
+
"Content-Type": "application/json",
|
|
13618
|
+
"User-Agent": "@hasna/events",
|
|
13619
|
+
"X-Hasna-Event-Id": event.id,
|
|
13620
|
+
"X-Hasna-Event-Type": event.type,
|
|
13621
|
+
"X-Hasna-Timestamp": timestamp,
|
|
13622
|
+
...channel.webhook.headers
|
|
13623
|
+
};
|
|
13624
|
+
if (channel.webhook.secret) {
|
|
13625
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
13626
|
+
}
|
|
13627
|
+
return { body, headers };
|
|
13628
|
+
}
|
|
13629
|
+
async function dispatchWebhook2(event, channel, options = {}) {
|
|
13630
|
+
if (!channel.webhook)
|
|
13631
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13632
|
+
const startedAt = now();
|
|
13633
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
13634
|
+
const controller = new AbortController;
|
|
13635
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
13636
|
+
try {
|
|
13637
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
13638
|
+
method: "POST",
|
|
13639
|
+
headers,
|
|
13640
|
+
body,
|
|
13641
|
+
signal: controller.signal
|
|
13642
|
+
});
|
|
13643
|
+
const responseBody = truncate(await response.text());
|
|
13644
|
+
return {
|
|
13645
|
+
attempt: 1,
|
|
13646
|
+
status: response.ok ? "success" : "failed",
|
|
13647
|
+
startedAt,
|
|
13648
|
+
completedAt: now(),
|
|
13649
|
+
responseStatus: response.status,
|
|
13650
|
+
responseBody,
|
|
13651
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
13652
|
+
};
|
|
13653
|
+
} catch (error) {
|
|
13654
|
+
return {
|
|
13655
|
+
attempt: 1,
|
|
13656
|
+
status: "failed",
|
|
13657
|
+
startedAt,
|
|
13658
|
+
completedAt: now(),
|
|
13659
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13660
|
+
};
|
|
13661
|
+
} finally {
|
|
13662
|
+
clearTimeout(timeout);
|
|
13663
|
+
}
|
|
13664
|
+
}
|
|
13665
|
+
async function dispatchCommand2(event, channel) {
|
|
13666
|
+
if (!channel.command)
|
|
13667
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
13668
|
+
const startedAt = now();
|
|
13669
|
+
const eventJson = JSON.stringify(event);
|
|
13670
|
+
const env = {
|
|
13671
|
+
...process.env,
|
|
13672
|
+
...channel.command.env,
|
|
13673
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
13674
|
+
HASNA_EVENT_ID: event.id,
|
|
13675
|
+
HASNA_EVENT_TYPE: event.type,
|
|
13676
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
13677
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
13678
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
13679
|
+
HASNA_EVENT_TIME: event.time,
|
|
13680
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
13681
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
13682
|
+
HASNA_EVENT_JSON: eventJson
|
|
13683
|
+
};
|
|
13684
|
+
return new Promise((resolve2) => {
|
|
13685
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
13686
|
+
cwd: channel.command.cwd,
|
|
13687
|
+
env,
|
|
13688
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
13689
|
+
});
|
|
13690
|
+
let stdout = "";
|
|
13691
|
+
let stderr = "";
|
|
13692
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
13693
|
+
child.stdin.end(eventJson);
|
|
13694
|
+
child.stdout.on("data", (chunk) => {
|
|
13695
|
+
stdout += chunk.toString();
|
|
13696
|
+
});
|
|
13697
|
+
child.stderr.on("data", (chunk) => {
|
|
13698
|
+
stderr += chunk.toString();
|
|
13699
|
+
});
|
|
13700
|
+
child.on("error", (error) => {
|
|
13701
|
+
clearTimeout(timeout);
|
|
13702
|
+
resolve2({
|
|
13703
|
+
attempt: 1,
|
|
13704
|
+
status: "failed",
|
|
13705
|
+
startedAt,
|
|
13706
|
+
completedAt: now(),
|
|
13707
|
+
stdout: truncate(stdout),
|
|
13708
|
+
stderr: truncate(stderr),
|
|
13709
|
+
error: error.message
|
|
13710
|
+
});
|
|
13711
|
+
});
|
|
13712
|
+
child.on("close", (code, signal) => {
|
|
13713
|
+
clearTimeout(timeout);
|
|
13714
|
+
const success = code === 0;
|
|
13715
|
+
resolve2({
|
|
13716
|
+
attempt: 1,
|
|
13717
|
+
status: success ? "success" : "failed",
|
|
13718
|
+
startedAt,
|
|
13719
|
+
completedAt: now(),
|
|
13720
|
+
stdout: truncate(stdout),
|
|
13721
|
+
stderr: truncate(stderr),
|
|
13722
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
13723
|
+
});
|
|
13724
|
+
});
|
|
13725
|
+
});
|
|
13726
|
+
}
|
|
13727
|
+
async function dispatchChannel2(event, channel, options = {}) {
|
|
13728
|
+
if (channel.transport === "webhook")
|
|
13729
|
+
return dispatchWebhook2(event, channel, options);
|
|
13730
|
+
if (channel.transport === "command")
|
|
13731
|
+
return dispatchCommand2(event, channel);
|
|
13732
|
+
return {
|
|
13733
|
+
attempt: 1,
|
|
13734
|
+
status: "skipped",
|
|
13735
|
+
startedAt: now(),
|
|
13736
|
+
completedAt: now(),
|
|
13737
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
13738
|
+
};
|
|
13739
|
+
}
|
|
13740
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
13741
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
13742
|
+
return {
|
|
13743
|
+
id: randomUUID(),
|
|
13744
|
+
eventId: event.id,
|
|
13745
|
+
channelId: channel.id,
|
|
13746
|
+
transport: channel.transport,
|
|
13747
|
+
status,
|
|
13748
|
+
attempts,
|
|
13749
|
+
createdAt: attempts[0]?.startedAt ?? now(),
|
|
13750
|
+
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
13751
|
+
};
|
|
13752
|
+
}
|
|
13753
|
+
function createEvent(input) {
|
|
13754
|
+
return {
|
|
13755
|
+
id: input.id ?? randomUUID2(),
|
|
13756
|
+
source: input.source,
|
|
13757
|
+
type: input.type,
|
|
13758
|
+
time: normalizeTime(input.time),
|
|
13759
|
+
subject: input.subject,
|
|
13760
|
+
severity: input.severity ?? "info",
|
|
13761
|
+
data: input.data ?? {},
|
|
13762
|
+
message: input.message,
|
|
13763
|
+
dedupeKey: input.dedupeKey,
|
|
13764
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
13765
|
+
metadata: input.metadata ?? {}
|
|
13766
|
+
};
|
|
13767
|
+
}
|
|
13768
|
+
|
|
13769
|
+
class EventsClient {
|
|
13770
|
+
store;
|
|
13771
|
+
redactors;
|
|
13772
|
+
transportOptions;
|
|
13773
|
+
constructor(options = {}) {
|
|
13774
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
13775
|
+
this.redactors = options.redactors ?? [];
|
|
13776
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
13777
|
+
}
|
|
13778
|
+
async addChannel(input) {
|
|
13779
|
+
const timestamp = new Date().toISOString();
|
|
13780
|
+
return this.store.addChannel({
|
|
13781
|
+
...input,
|
|
13782
|
+
createdAt: input.createdAt ?? timestamp,
|
|
13783
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
13784
|
+
});
|
|
13785
|
+
}
|
|
13786
|
+
async listChannels() {
|
|
13787
|
+
return this.store.listChannels();
|
|
13788
|
+
}
|
|
13789
|
+
async removeChannel(id) {
|
|
13790
|
+
return this.store.removeChannel(id);
|
|
13791
|
+
}
|
|
13792
|
+
async emit(input, options = {}) {
|
|
13793
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
13794
|
+
if (options.dedupe !== false) {
|
|
13795
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
13796
|
+
if (existing) {
|
|
13797
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
13798
|
+
}
|
|
13799
|
+
}
|
|
13800
|
+
await this.store.appendEvent(event);
|
|
13801
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
13802
|
+
return { event, deliveries, deduped: false };
|
|
13803
|
+
}
|
|
13804
|
+
async listEvents() {
|
|
13805
|
+
return this.store.listEvents();
|
|
13806
|
+
}
|
|
13807
|
+
async listDeliveries() {
|
|
13808
|
+
return this.store.listDeliveries();
|
|
13809
|
+
}
|
|
13810
|
+
async deliver(event) {
|
|
13811
|
+
const channels = await this.store.listChannels();
|
|
13812
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
13813
|
+
const deliveries = [];
|
|
13814
|
+
for (const channel of selected) {
|
|
13815
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13816
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13817
|
+
await this.store.appendDelivery(result);
|
|
13818
|
+
deliveries.push(result);
|
|
13819
|
+
}
|
|
13820
|
+
return deliveries;
|
|
13821
|
+
}
|
|
13822
|
+
async testChannel(id, input = {}) {
|
|
13823
|
+
const channel = await this.store.getChannel(id);
|
|
13824
|
+
if (!channel)
|
|
13825
|
+
throw new Error(`Channel not found: ${id}`);
|
|
13826
|
+
const event = createEvent({
|
|
13827
|
+
source: input.source ?? "hasna.events",
|
|
13828
|
+
type: input.type ?? "events.test",
|
|
13829
|
+
subject: input.subject ?? id,
|
|
13830
|
+
severity: input.severity ?? "info",
|
|
13831
|
+
data: input.data ?? { test: true },
|
|
13832
|
+
message: input.message ?? "Hasna events test delivery",
|
|
13833
|
+
dedupeKey: input.dedupeKey,
|
|
13834
|
+
schemaVersion: input.schemaVersion,
|
|
13835
|
+
metadata: input.metadata,
|
|
13836
|
+
time: input.time,
|
|
13837
|
+
id: input.id
|
|
13838
|
+
});
|
|
13839
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13840
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13841
|
+
await this.store.appendDelivery(result);
|
|
13842
|
+
return result;
|
|
13843
|
+
}
|
|
13844
|
+
async replay(options = {}) {
|
|
13845
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
13846
|
+
if (options.eventId && event.id !== options.eventId)
|
|
13847
|
+
return false;
|
|
13848
|
+
if (options.source && event.source !== options.source)
|
|
13849
|
+
return false;
|
|
13850
|
+
if (options.type && event.type !== options.type)
|
|
13851
|
+
return false;
|
|
13852
|
+
return true;
|
|
13853
|
+
});
|
|
13854
|
+
if (options.dryRun)
|
|
13855
|
+
return { events, deliveries: [] };
|
|
13856
|
+
const deliveries = [];
|
|
13857
|
+
for (const event of events) {
|
|
13858
|
+
deliveries.push(...await this.deliver(event));
|
|
13859
|
+
}
|
|
13860
|
+
return { events, deliveries };
|
|
13861
|
+
}
|
|
13862
|
+
async applyRedaction(event, channel) {
|
|
13863
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
13864
|
+
for (const redactor of this.redactors) {
|
|
13865
|
+
next = await redactor(next, channel);
|
|
13866
|
+
}
|
|
13867
|
+
return next;
|
|
13868
|
+
}
|
|
13869
|
+
async deliverWithRetry(event, channel) {
|
|
13870
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
13871
|
+
const attempts = [];
|
|
13872
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
13873
|
+
const attempt = await dispatchChannel2(event, channel, this.transportOptions);
|
|
13874
|
+
attempt.attempt = index + 1;
|
|
13875
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
13876
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
13877
|
+
}
|
|
13878
|
+
attempts.push(attempt);
|
|
13879
|
+
if (attempt.status !== "failed")
|
|
13880
|
+
break;
|
|
13881
|
+
if (attempt.nextBackoffMs)
|
|
13882
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
13883
|
+
}
|
|
13884
|
+
return createDeliveryResult(event, channel, attempts);
|
|
13885
|
+
}
|
|
13886
|
+
}
|
|
13887
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
13888
|
+
if (paths.length === 0)
|
|
13889
|
+
return event;
|
|
13890
|
+
const copy = structuredClone(event);
|
|
13891
|
+
for (const path of paths) {
|
|
13892
|
+
setPath(copy, path, replacement);
|
|
13893
|
+
}
|
|
13894
|
+
return copy;
|
|
13895
|
+
}
|
|
13896
|
+
function sanitizeChannelForOutput(channel) {
|
|
13897
|
+
const copy = structuredClone(channel);
|
|
13898
|
+
if (copy.webhook?.secret)
|
|
13899
|
+
copy.webhook.secret = "[REDACTED]";
|
|
13900
|
+
if (copy.command?.env) {
|
|
13901
|
+
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
13902
|
+
}
|
|
13903
|
+
return copy;
|
|
13904
|
+
}
|
|
13905
|
+
function sanitizeChannelsForOutput(channels) {
|
|
13906
|
+
return channels.map(sanitizeChannelForOutput);
|
|
13907
|
+
}
|
|
13908
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
13909
|
+
return redactValue(event, replacement);
|
|
13910
|
+
}
|
|
13911
|
+
function shouldRedactKey(key) {
|
|
13912
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
13913
|
+
}
|
|
13914
|
+
function redactValue(value, replacement) {
|
|
13915
|
+
if (Array.isArray(value))
|
|
13916
|
+
return value.map((item) => redactValue(item, replacement));
|
|
13917
|
+
if (!value || typeof value !== "object")
|
|
13918
|
+
return value;
|
|
13919
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
13920
|
+
key,
|
|
13921
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
13922
|
+
]));
|
|
13923
|
+
}
|
|
13924
|
+
function setPath(input, path, replacement) {
|
|
13925
|
+
const parts = path.split(".");
|
|
13926
|
+
let cursor = input;
|
|
13927
|
+
for (const part of parts.slice(0, -1)) {
|
|
13928
|
+
const next = cursor[part];
|
|
13929
|
+
if (!next || typeof next !== "object")
|
|
13930
|
+
return;
|
|
13931
|
+
cursor = next;
|
|
13932
|
+
}
|
|
13933
|
+
const last = parts.at(-1);
|
|
13934
|
+
if (last && last in cursor)
|
|
13935
|
+
cursor[last] = replacement;
|
|
13936
|
+
}
|
|
13937
|
+
function normalizeTime(value) {
|
|
13938
|
+
if (!value)
|
|
13939
|
+
return new Date().toISOString();
|
|
13940
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
13941
|
+
}
|
|
13942
|
+
function normalizeRetryPolicy(policy) {
|
|
13943
|
+
return {
|
|
13944
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
13945
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
13946
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
13947
|
+
};
|
|
13948
|
+
}
|
|
13949
|
+
|
|
13430
13950
|
// src/commands/status.ts
|
|
13431
13951
|
function getStatus() {
|
|
13432
13952
|
const manifest = readManifest();
|
|
@@ -13476,13 +13996,16 @@ function getServeInfo(options = {}) {
|
|
|
13476
13996
|
"/api/status",
|
|
13477
13997
|
"/api/manifest",
|
|
13478
13998
|
"/api/notifications",
|
|
13999
|
+
"/api/webhooks",
|
|
14000
|
+
"/api/events",
|
|
13479
14001
|
"/api/doctor",
|
|
13480
14002
|
"/api/self-test",
|
|
13481
14003
|
"/api/apps/status",
|
|
13482
14004
|
"/api/apps/diff",
|
|
13483
14005
|
"/api/install-claude/status",
|
|
13484
14006
|
"/api/install-claude/diff",
|
|
13485
|
-
"/api/notifications/test"
|
|
14007
|
+
"/api/notifications/test",
|
|
14008
|
+
"/api/webhooks/test"
|
|
13486
14009
|
]
|
|
13487
14010
|
};
|
|
13488
14011
|
}
|
|
@@ -13649,6 +14172,7 @@ function jsonError(message, status = 400) {
|
|
|
13649
14172
|
}
|
|
13650
14173
|
function startDashboardServer(options = {}) {
|
|
13651
14174
|
const info = getServeInfo(options);
|
|
14175
|
+
const events = new EventsClient;
|
|
13652
14176
|
return Bun.serve({
|
|
13653
14177
|
hostname: info.host,
|
|
13654
14178
|
port: info.port,
|
|
@@ -13668,6 +14192,42 @@ function startDashboardServer(options = {}) {
|
|
|
13668
14192
|
if (url.pathname === "/api/notifications") {
|
|
13669
14193
|
return Response.json(listNotificationChannels());
|
|
13670
14194
|
}
|
|
14195
|
+
if (url.pathname === "/api/webhooks") {
|
|
14196
|
+
if (request.method !== "GET") {
|
|
14197
|
+
return jsonError("Use GET for webhook channel listing.", 405);
|
|
14198
|
+
}
|
|
14199
|
+
return Response.json(sanitizeChannelsForOutput(await events.listChannels()));
|
|
14200
|
+
}
|
|
14201
|
+
if (url.pathname === "/api/events") {
|
|
14202
|
+
if (request.method === "GET") {
|
|
14203
|
+
return Response.json(await events.listEvents());
|
|
14204
|
+
}
|
|
14205
|
+
if (request.method !== "POST") {
|
|
14206
|
+
return jsonError("Use GET or POST for events.", 405);
|
|
14207
|
+
}
|
|
14208
|
+
const body = await parseJsonBody(request);
|
|
14209
|
+
const type = typeof body["type"] === "string" ? body["type"] : undefined;
|
|
14210
|
+
if (!type) {
|
|
14211
|
+
return jsonError("type is required.");
|
|
14212
|
+
}
|
|
14213
|
+
const source = typeof body["source"] === "string" ? body["source"] : "machines";
|
|
14214
|
+
const subject = typeof body["subject"] === "string" ? body["subject"] : undefined;
|
|
14215
|
+
const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
|
|
14216
|
+
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
14217
|
+
const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
|
|
14218
|
+
const data = body["data"] && typeof body["data"] === "object" && !Array.isArray(body["data"]) ? body["data"] : {};
|
|
14219
|
+
const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? body["metadata"] : {};
|
|
14220
|
+
return Response.json(await events.emit({
|
|
14221
|
+
source,
|
|
14222
|
+
type,
|
|
14223
|
+
subject,
|
|
14224
|
+
severity,
|
|
14225
|
+
message,
|
|
14226
|
+
dedupeKey,
|
|
14227
|
+
data,
|
|
14228
|
+
metadata
|
|
14229
|
+
}));
|
|
14230
|
+
}
|
|
13671
14231
|
if (url.pathname === "/api/doctor") {
|
|
13672
14232
|
return Response.json(runDoctor(machineId));
|
|
13673
14233
|
}
|
|
@@ -13705,6 +14265,28 @@ function startDashboardServer(options = {}) {
|
|
|
13705
14265
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
13706
14266
|
}
|
|
13707
14267
|
}
|
|
14268
|
+
if (url.pathname === "/api/webhooks/test") {
|
|
14269
|
+
if (request.method !== "POST") {
|
|
14270
|
+
return jsonError("Use POST for webhook tests.", 405);
|
|
14271
|
+
}
|
|
14272
|
+
const body = await parseJsonBody(request);
|
|
14273
|
+
const channelId = typeof body["channelId"] === "string" ? body["channelId"] : undefined;
|
|
14274
|
+
if (!channelId) {
|
|
14275
|
+
return jsonError("channelId is required.");
|
|
14276
|
+
}
|
|
14277
|
+
const type = typeof body["type"] === "string" ? body["type"] : "events.test";
|
|
14278
|
+
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
14279
|
+
try {
|
|
14280
|
+
return Response.json(await events.testChannel(channelId, {
|
|
14281
|
+
source: "machines",
|
|
14282
|
+
type,
|
|
14283
|
+
subject: channelId,
|
|
14284
|
+
message
|
|
14285
|
+
}));
|
|
14286
|
+
} catch (error) {
|
|
14287
|
+
return jsonError(error instanceof Error ? error.message : String(error));
|
|
14288
|
+
}
|
|
14289
|
+
}
|
|
13708
14290
|
return new Response(renderDashboardHtml(), {
|
|
13709
14291
|
headers: {
|
|
13710
14292
|
"content-type": "text/html; charset=utf-8"
|
|
@@ -13744,7 +14326,7 @@ function runSelfTest() {
|
|
|
13744
14326
|
};
|
|
13745
14327
|
}
|
|
13746
14328
|
// src/commands/setup.ts
|
|
13747
|
-
import { homedir as
|
|
14329
|
+
import { homedir as homedir5 } from "os";
|
|
13748
14330
|
function quote3(value) {
|
|
13749
14331
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
13750
14332
|
}
|
|
@@ -13814,7 +14396,7 @@ function buildSetupPlan(machineId) {
|
|
|
13814
14396
|
const target = selected || {
|
|
13815
14397
|
id: currentMachineId,
|
|
13816
14398
|
platform: "linux",
|
|
13817
|
-
workspacePath: `${
|
|
14399
|
+
workspacePath: `${homedir5()}/workspace`
|
|
13818
14400
|
};
|
|
13819
14401
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
13820
14402
|
return {
|
|
@@ -13859,15 +14441,32 @@ function runSetup(machineId, options = {}) {
|
|
|
13859
14441
|
return summary;
|
|
13860
14442
|
}
|
|
13861
14443
|
// src/commands/screen.ts
|
|
14444
|
+
var DEFAULT_SCREEN_SECRET_NAMESPACE = "hasna/xyz/opensource/machines/prod";
|
|
13862
14445
|
function shellQuote7(value) {
|
|
13863
14446
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
13864
14447
|
}
|
|
14448
|
+
function shellCommand2(command) {
|
|
14449
|
+
return command.map(shellQuote7).join(" ");
|
|
14450
|
+
}
|
|
14451
|
+
function metadataString2(metadata, keys) {
|
|
14452
|
+
if (!metadata)
|
|
14453
|
+
return null;
|
|
14454
|
+
for (const key of keys) {
|
|
14455
|
+
const value = metadata[key];
|
|
14456
|
+
if (typeof value === "string" && value.trim())
|
|
14457
|
+
return value.trim();
|
|
14458
|
+
}
|
|
14459
|
+
return null;
|
|
14460
|
+
}
|
|
13865
14461
|
function splitTarget(target) {
|
|
13866
14462
|
const at = target.indexOf("@");
|
|
13867
14463
|
if (at === -1)
|
|
13868
14464
|
return [null, target];
|
|
13869
14465
|
return [target.slice(0, at), target.slice(at + 1)];
|
|
13870
14466
|
}
|
|
14467
|
+
function defaultScreenPasswordSecretKey(machineId) {
|
|
14468
|
+
return `${DEFAULT_SCREEN_SECRET_NAMESPACE}/screen-${machineId}-vnc-password`;
|
|
14469
|
+
}
|
|
13871
14470
|
function resolveScreenTarget(machineId, options = {}) {
|
|
13872
14471
|
const resolved = resolveMachineRoute(machineId, options);
|
|
13873
14472
|
if (!resolved.ok || !resolved.target) {
|
|
@@ -13893,6 +14492,30 @@ function resolveScreenTarget(machineId, options = {}) {
|
|
|
13893
14492
|
warnings: resolved.warnings
|
|
13894
14493
|
};
|
|
13895
14494
|
}
|
|
14495
|
+
function resolveScreenCredentials(machineId, options = {}) {
|
|
14496
|
+
const topology = options.topology ?? discoverMachineTopology(options);
|
|
14497
|
+
const screen = resolveScreenTarget(machineId, { ...options, topology });
|
|
14498
|
+
const entry = topology.machines.find((machine) => machine.machine_id === screen.machineId);
|
|
14499
|
+
const metadata = entry?.metadata;
|
|
14500
|
+
const metadataUser = metadataString2(metadata, ["screenUser", "screen_user", "user", "username"]);
|
|
14501
|
+
const metadataPasswordSecret = metadataString2(metadata, [
|
|
14502
|
+
"screenPasswordSecret",
|
|
14503
|
+
"screen_password_secret",
|
|
14504
|
+
"screenVncPasswordSecret",
|
|
14505
|
+
"screen_vnc_password_secret",
|
|
14506
|
+
"vncPasswordSecret",
|
|
14507
|
+
"vnc_password_secret"
|
|
14508
|
+
]);
|
|
14509
|
+
const user = options.user ?? screen.user ?? metadataUser;
|
|
14510
|
+
const passwordSecretKey = options.passwordSecretKey ?? metadataPasswordSecret ?? defaultScreenPasswordSecretKey(screen.machineId);
|
|
14511
|
+
return {
|
|
14512
|
+
machineId: screen.machineId,
|
|
14513
|
+
user: user ?? null,
|
|
14514
|
+
userSource: options.user ? "option" : screen.user ? "route" : metadataUser ? "metadata" : "missing",
|
|
14515
|
+
passwordSecretKey,
|
|
14516
|
+
passwordSecretSource: options.passwordSecretKey ? "option" : metadataPasswordSecret ? "metadata" : "default"
|
|
14517
|
+
};
|
|
14518
|
+
}
|
|
13896
14519
|
function buildScreenCommand(machineId, options = {}) {
|
|
13897
14520
|
const resolved = resolveScreenTarget(machineId, options);
|
|
13898
14521
|
return `open ${resolved.url}`;
|
|
@@ -13907,9 +14530,40 @@ function buildScreenEnableRemoteCommand(user, vncPassword) {
|
|
|
13907
14530
|
];
|
|
13908
14531
|
return lines.join(" && ");
|
|
13909
14532
|
}
|
|
14533
|
+
function buildScreenEnableRemoteCommandFromStdin(user) {
|
|
14534
|
+
const kickstart = "/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart";
|
|
14535
|
+
const script = [
|
|
14536
|
+
"set -euo pipefail",
|
|
14537
|
+
'user="$1"',
|
|
14538
|
+
"IFS= read -r vnc_pw",
|
|
14539
|
+
'if [ -z "$vnc_pw" ]; then echo "missing VNC password on stdin" >&2; exit 1; fi',
|
|
14540
|
+
`kickstart=${shellQuote7(kickstart)}`,
|
|
14541
|
+
'dseditgroup -o edit -a "$user" -t user com.apple.access_screensharing 2>/dev/null || true',
|
|
14542
|
+
"defaults write /Library/Preferences/com.apple.RemoteManagement AllowSRPForNetworkNodes -bool true",
|
|
14543
|
+
'"$kickstart" -configure -clientopts -setvnclegacy -vnclegacy yes -setvncpw -vncpw "$vnc_pw"',
|
|
14544
|
+
'"$kickstart" -activate -configure -access -on -users "$user" -privs -all -restart -agent -menu'
|
|
14545
|
+
].join(`
|
|
14546
|
+
`);
|
|
14547
|
+
return `sudo -n -p '' /bin/bash -c ${shellQuote7(script)} -- ${shellQuote7(user)}`;
|
|
14548
|
+
}
|
|
14549
|
+
function buildScreenEnableCommand(machineId, options = {}) {
|
|
14550
|
+
const credentials = resolveScreenCredentials(machineId, options);
|
|
14551
|
+
if (!credentials.user) {
|
|
14552
|
+
throw new Error(`No screen-sharing user known for ${machineId}; pass --user <name> or set metadata.user in the manifest.`);
|
|
14553
|
+
}
|
|
14554
|
+
const secretsCommand = options.secretsCommand || "secrets";
|
|
14555
|
+
const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
|
|
14556
|
+
return {
|
|
14557
|
+
machineId: credentials.machineId,
|
|
14558
|
+
user: credentials.user,
|
|
14559
|
+
passwordSecretKey: credentials.passwordSecretKey,
|
|
14560
|
+
remoteCommand,
|
|
14561
|
+
command: `${shellCommand2([secretsCommand, "get", credentials.passwordSecretKey])} | ${buildSshCommand(machineId, remoteCommand, options)}`
|
|
14562
|
+
};
|
|
14563
|
+
}
|
|
13910
14564
|
// src/commands/sync.ts
|
|
13911
|
-
import { existsSync as
|
|
13912
|
-
import { homedir as
|
|
14565
|
+
import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
14566
|
+
import { homedir as homedir6 } from "os";
|
|
13913
14567
|
function quote4(value) {
|
|
13914
14568
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
13915
14569
|
}
|
|
@@ -13957,8 +14611,8 @@ function detectPackageActions(machine) {
|
|
|
13957
14611
|
}
|
|
13958
14612
|
function detectFileActions(machine) {
|
|
13959
14613
|
return (machine.files || []).map((file, index) => {
|
|
13960
|
-
const sourceExists =
|
|
13961
|
-
const targetExists =
|
|
14614
|
+
const sourceExists = existsSync8(file.source);
|
|
14615
|
+
const targetExists = existsSync8(file.target);
|
|
13962
14616
|
let status = "missing";
|
|
13963
14617
|
if (sourceExists && targetExists) {
|
|
13964
14618
|
if (file.mode === "symlink") {
|
|
@@ -13986,7 +14640,7 @@ function buildSyncPlan(machineId) {
|
|
|
13986
14640
|
const target = selected || {
|
|
13987
14641
|
id: currentMachineId,
|
|
13988
14642
|
platform: "linux",
|
|
13989
|
-
workspacePath: `${
|
|
14643
|
+
workspacePath: `${homedir6()}/workspace`
|
|
13990
14644
|
};
|
|
13991
14645
|
const actions = [
|
|
13992
14646
|
...detectPackageActions(target),
|
|
@@ -23160,6 +23814,13 @@ var MACHINE_MCP_TOOL_NAMES = [
|
|
|
23160
23814
|
"machines_notifications_test",
|
|
23161
23815
|
"machines_notifications_dispatch",
|
|
23162
23816
|
"machines_notifications_remove",
|
|
23817
|
+
"machines_webhooks_add",
|
|
23818
|
+
"machines_webhooks_list",
|
|
23819
|
+
"machines_webhooks_test",
|
|
23820
|
+
"machines_webhooks_remove",
|
|
23821
|
+
"machines_events_emit",
|
|
23822
|
+
"machines_events_list",
|
|
23823
|
+
"machines_events_replay",
|
|
23163
23824
|
"machines_serve_info",
|
|
23164
23825
|
"machines_serve_dashboard",
|
|
23165
23826
|
"storage_status",
|
|
@@ -23172,6 +23833,7 @@ function buildServer(version2 = getPackageVersion()) {
|
|
|
23172
23833
|
}
|
|
23173
23834
|
function createMcpServer(version2) {
|
|
23174
23835
|
const server = new McpServer({ name: "machines", version: version2 });
|
|
23836
|
+
const events = new EventsClient;
|
|
23175
23837
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
23176
23838
|
content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
|
|
23177
23839
|
}));
|
|
@@ -23314,8 +23976,8 @@ function createMcpServer(version2) {
|
|
|
23314
23976
|
target: exports_external.string().describe("Email, webhook URL, or shell command"),
|
|
23315
23977
|
events: exports_external.array(exports_external.string()).describe("Events routed to this channel"),
|
|
23316
23978
|
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
23317
|
-
}, async ({ channel_id, type, target, events, enabled }) => ({
|
|
23318
|
-
content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events, enabled: enabled ?? true }), null, 2) }]
|
|
23979
|
+
}, async ({ channel_id, type, target, events: events2, enabled }) => ({
|
|
23980
|
+
content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events: events2, enabled: enabled ?? true }), null, 2) }]
|
|
23319
23981
|
}));
|
|
23320
23982
|
server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
|
|
23321
23983
|
content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
|
|
@@ -23325,6 +23987,60 @@ function createMcpServer(version2) {
|
|
|
23325
23987
|
}));
|
|
23326
23988
|
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) }] }));
|
|
23327
23989
|
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) }] }));
|
|
23990
|
+
server.tool("machines_webhooks_add", "Add or replace a shared Hasna event webhook channel.", {
|
|
23991
|
+
channel_id: exports_external.string().describe("Channel identifier"),
|
|
23992
|
+
url: exports_external.string().url().describe("Webhook URL"),
|
|
23993
|
+
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
23994
|
+
source: exports_external.string().optional().describe("Optional source filter"),
|
|
23995
|
+
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
23996
|
+
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
23997
|
+
}, async ({ channel_id, url, event_type, source, secret, enabled }) => {
|
|
23998
|
+
const now2 = new Date().toISOString();
|
|
23999
|
+
const channel = await events.addChannel({
|
|
24000
|
+
id: channel_id,
|
|
24001
|
+
enabled: enabled ?? true,
|
|
24002
|
+
transport: "webhook",
|
|
24003
|
+
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
24004
|
+
webhook: { url, secret },
|
|
24005
|
+
createdAt: now2,
|
|
24006
|
+
updatedAt: now2
|
|
24007
|
+
});
|
|
24008
|
+
return { content: [{ type: "text", text: JSON.stringify(sanitizeChannelForOutput(channel), null, 2) }] };
|
|
24009
|
+
});
|
|
24010
|
+
server.tool("machines_webhooks_list", "List shared Hasna event webhook channels.", {}, async () => ({
|
|
24011
|
+
content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput(await events.listChannels()), null, 2) }]
|
|
24012
|
+
}));
|
|
24013
|
+
server.tool("machines_webhooks_test", "Send a test event to one shared Hasna 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 }) => ({
|
|
24014
|
+
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
24015
|
+
}));
|
|
24016
|
+
server.tool("machines_webhooks_remove", "Remove a shared Hasna 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) }] }));
|
|
24017
|
+
server.tool("machines_events_emit", "Emit a shared Hasna event from machines.", {
|
|
24018
|
+
event_type: exports_external.string().describe("Event type"),
|
|
24019
|
+
subject: exports_external.string().optional().describe("Event subject"),
|
|
24020
|
+
severity: exports_external.enum(["debug", "info", "notice", "warning", "error", "critical"]).optional().describe("Event severity"),
|
|
24021
|
+
message: exports_external.string().optional().describe("Message body"),
|
|
24022
|
+
data: exports_external.record(exports_external.unknown()).optional().describe("Event data"),
|
|
24023
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Event metadata"),
|
|
24024
|
+
dedupe_key: exports_external.string().optional().describe("Dedupe key"),
|
|
24025
|
+
deliver: exports_external.boolean().optional().describe("Deliver to matching channels")
|
|
24026
|
+
}, async ({ event_type, subject, severity, message, data, metadata, dedupe_key, deliver }) => ({
|
|
24027
|
+
content: [{ type: "text", text: JSON.stringify(await events.emit({
|
|
24028
|
+
source: "machines",
|
|
24029
|
+
type: event_type,
|
|
24030
|
+
subject,
|
|
24031
|
+
severity,
|
|
24032
|
+
message,
|
|
24033
|
+
data: data ?? {},
|
|
24034
|
+
metadata: metadata ?? {},
|
|
24035
|
+
dedupeKey: dedupe_key
|
|
24036
|
+
}, { deliver: deliver !== false }), null, 2) }]
|
|
24037
|
+
}));
|
|
24038
|
+
server.tool("machines_events_list", "List shared Hasna events.", {}, async () => ({
|
|
24039
|
+
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
24040
|
+
}));
|
|
24041
|
+
server.tool("machines_events_replay", "Replay shared Hasna 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 }) => ({
|
|
24042
|
+
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
24043
|
+
}));
|
|
23328
24044
|
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) }] }));
|
|
23329
24045
|
server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
|
|
23330
24046
|
content: [{ type: "text", text: renderDashboardHtml() }]
|
|
@@ -23362,6 +24078,7 @@ export {
|
|
|
23362
24078
|
resolveTables,
|
|
23363
24079
|
resolveSshTarget,
|
|
23364
24080
|
resolveScreenTarget,
|
|
24081
|
+
resolveScreenCredentials,
|
|
23365
24082
|
resolveMachineWorkspace,
|
|
23366
24083
|
resolveMachineRoute,
|
|
23367
24084
|
resolveBackupTarget,
|
|
@@ -23425,6 +24142,7 @@ export {
|
|
|
23425
24142
|
diffClaudeCli,
|
|
23426
24143
|
diffApps,
|
|
23427
24144
|
detectCurrentMachineManifest,
|
|
24145
|
+
defaultScreenPasswordSecretKey,
|
|
23428
24146
|
createMcpServer,
|
|
23429
24147
|
createMachineResolverSnapshot,
|
|
23430
24148
|
countRuns,
|
|
@@ -23435,7 +24153,9 @@ export {
|
|
|
23435
24153
|
buildSshCommand,
|
|
23436
24154
|
buildSetupPlan,
|
|
23437
24155
|
buildServer,
|
|
24156
|
+
buildScreenEnableRemoteCommandFromStdin,
|
|
23438
24157
|
buildScreenEnableRemoteCommand,
|
|
24158
|
+
buildScreenEnableCommand,
|
|
23439
24159
|
buildScreenCommand,
|
|
23440
24160
|
buildClaudeInstallPlan,
|
|
23441
24161
|
buildCertPlan,
|
|
@@ -23465,6 +24185,7 @@ export {
|
|
|
23465
24185
|
MACHINES_BACKUP_PREFIX_ENV,
|
|
23466
24186
|
MACHINES_BACKUP_BUCKET_FALLBACK_ENV,
|
|
23467
24187
|
MACHINES_BACKUP_BUCKET_ENV,
|
|
24188
|
+
DEFAULT_SCREEN_SECRET_NAMESPACE,
|
|
23468
24189
|
DEFAULT_MACHINE_RESOLVER_TTL_MS,
|
|
23469
24190
|
DEFAULT_BACKUP_PREFIX,
|
|
23470
24191
|
CROSSREFS_KEY
|