@hasna/machines 0.0.31 → 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 +157 -1321
- package/dist/commands/apps.d.ts.map +1 -1
- package/dist/commands/install-claude.d.ts.map +1 -1
- package/dist/commands/install-tailscale.d.ts.map +1 -1
- package/dist/commands/screen.d.ts +2 -1
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/index.js +57 -547
- package/dist/mcp/index.js +110 -604
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -12645,7 +12645,14 @@ function buildAppSteps(machine) {
|
|
|
12645
12645
|
}));
|
|
12646
12646
|
}
|
|
12647
12647
|
function resolveMachine(machineId) {
|
|
12648
|
-
|
|
12648
|
+
if (!machineId)
|
|
12649
|
+
return detectCurrentMachineManifest();
|
|
12650
|
+
return getManifestMachine(machineId) || {
|
|
12651
|
+
id: machineId,
|
|
12652
|
+
platform: "linux",
|
|
12653
|
+
workspacePath: "",
|
|
12654
|
+
apps: []
|
|
12655
|
+
};
|
|
12649
12656
|
}
|
|
12650
12657
|
function parseProbeOutput(app, machine, stdout) {
|
|
12651
12658
|
const lines = stdout.trim().split(`
|
|
@@ -12995,7 +13002,13 @@ function buildInstallSteps(machine, tools) {
|
|
|
12995
13002
|
}));
|
|
12996
13003
|
}
|
|
12997
13004
|
function resolveMachine2(machineId) {
|
|
12998
|
-
|
|
13005
|
+
if (!machineId)
|
|
13006
|
+
return detectCurrentMachineManifest();
|
|
13007
|
+
return getManifestMachine(machineId) || {
|
|
13008
|
+
id: machineId,
|
|
13009
|
+
platform: "linux",
|
|
13010
|
+
workspacePath: ""
|
|
13011
|
+
};
|
|
12999
13012
|
}
|
|
13000
13013
|
function buildProbeCommand(tool) {
|
|
13001
13014
|
const binary = getToolBinary(tool);
|
|
@@ -13095,7 +13108,10 @@ function buildInstallSteps2(machine) {
|
|
|
13095
13108
|
];
|
|
13096
13109
|
}
|
|
13097
13110
|
function buildTailscaleInstallPlan(machineId) {
|
|
13098
|
-
const machine =
|
|
13111
|
+
const machine = machineId ? getManifestMachine(machineId) : detectCurrentMachineManifest();
|
|
13112
|
+
if (!machine) {
|
|
13113
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
13114
|
+
}
|
|
13099
13115
|
return {
|
|
13100
13116
|
machineId: machine.id,
|
|
13101
13117
|
mode: "plan",
|
|
@@ -13424,528 +13440,7 @@ function listPorts(machineId) {
|
|
|
13424
13440
|
// src/commands/runtime.ts
|
|
13425
13441
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
13426
13442
|
import { setTimeout as sleep } from "timers/promises";
|
|
13427
|
-
|
|
13428
|
-
// node_modules/@hasna/events/dist/index.js
|
|
13429
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
13430
|
-
import { existsSync as existsSync7 } from "fs";
|
|
13431
|
-
import { homedir as homedir4 } from "os";
|
|
13432
|
-
import { join as join6 } from "path";
|
|
13433
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
13434
|
-
import { randomUUID } from "crypto";
|
|
13435
|
-
import { spawn } from "child_process";
|
|
13436
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
13437
|
-
function getPathValue(input, path) {
|
|
13438
|
-
return path.split(".").reduce((value, part) => {
|
|
13439
|
-
if (value && typeof value === "object" && part in value) {
|
|
13440
|
-
return value[part];
|
|
13441
|
-
}
|
|
13442
|
-
return;
|
|
13443
|
-
}, input);
|
|
13444
|
-
}
|
|
13445
|
-
function wildcardToRegExp(pattern) {
|
|
13446
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
13447
|
-
return new RegExp(`^${escaped}$`);
|
|
13448
|
-
}
|
|
13449
|
-
function matchString(value, matcher) {
|
|
13450
|
-
if (matcher === undefined)
|
|
13451
|
-
return true;
|
|
13452
|
-
if (value === undefined)
|
|
13453
|
-
return false;
|
|
13454
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
13455
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
13456
|
-
}
|
|
13457
|
-
function matchRecord(input, matcher) {
|
|
13458
|
-
if (!matcher)
|
|
13459
|
-
return true;
|
|
13460
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
13461
|
-
const actual = getPathValue(input, path);
|
|
13462
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
13463
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
13464
|
-
}
|
|
13465
|
-
return actual === expected;
|
|
13466
|
-
});
|
|
13467
|
-
}
|
|
13468
|
-
function eventMatchesFilter(event, filter) {
|
|
13469
|
-
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);
|
|
13470
|
-
}
|
|
13471
|
-
function channelMatchesEvent(channel, event) {
|
|
13472
|
-
if (!channel.enabled)
|
|
13473
|
-
return false;
|
|
13474
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
13475
|
-
return true;
|
|
13476
|
-
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
13477
|
-
}
|
|
13478
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
13479
|
-
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
13480
|
-
function getEventsDataDir(override) {
|
|
13481
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join6(homedir4(), ".hasna", "events");
|
|
13482
|
-
}
|
|
13483
|
-
|
|
13484
|
-
class JsonEventsStore {
|
|
13485
|
-
dataDir;
|
|
13486
|
-
channelsPath;
|
|
13487
|
-
eventsPath;
|
|
13488
|
-
deliveriesPath;
|
|
13489
|
-
constructor(dataDir = getEventsDataDir()) {
|
|
13490
|
-
this.dataDir = dataDir;
|
|
13491
|
-
this.channelsPath = join6(dataDir, "channels.json");
|
|
13492
|
-
this.eventsPath = join6(dataDir, "events.json");
|
|
13493
|
-
this.deliveriesPath = join6(dataDir, "deliveries.json");
|
|
13494
|
-
}
|
|
13495
|
-
async init() {
|
|
13496
|
-
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
13497
|
-
await chmod(this.dataDir, 448).catch(() => {
|
|
13498
|
-
return;
|
|
13499
|
-
});
|
|
13500
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
13501
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
13502
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
13503
|
-
}
|
|
13504
|
-
async addChannel(channel) {
|
|
13505
|
-
await this.init();
|
|
13506
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
13507
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
13508
|
-
if (index >= 0) {
|
|
13509
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
13510
|
-
} else {
|
|
13511
|
-
channels.push(channel);
|
|
13512
|
-
}
|
|
13513
|
-
await this.writeJson(this.channelsPath, channels);
|
|
13514
|
-
return index >= 0 ? channels[index] : channel;
|
|
13515
|
-
}
|
|
13516
|
-
async listChannels() {
|
|
13517
|
-
await this.init();
|
|
13518
|
-
return this.readJson(this.channelsPath, []);
|
|
13519
|
-
}
|
|
13520
|
-
async getChannel(id) {
|
|
13521
|
-
const channels = await this.listChannels();
|
|
13522
|
-
return channels.find((channel) => channel.id === id);
|
|
13523
|
-
}
|
|
13524
|
-
async removeChannel(id) {
|
|
13525
|
-
await this.init();
|
|
13526
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
13527
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
13528
|
-
await this.writeJson(this.channelsPath, next);
|
|
13529
|
-
return next.length !== channels.length;
|
|
13530
|
-
}
|
|
13531
|
-
async appendEvent(event) {
|
|
13532
|
-
await this.init();
|
|
13533
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
13534
|
-
events.push(event);
|
|
13535
|
-
await this.writeJson(this.eventsPath, events);
|
|
13536
|
-
return event;
|
|
13537
|
-
}
|
|
13538
|
-
async listEvents() {
|
|
13539
|
-
await this.init();
|
|
13540
|
-
return this.readJson(this.eventsPath, []);
|
|
13541
|
-
}
|
|
13542
|
-
async findEventByIdentity(identity) {
|
|
13543
|
-
const events = await this.listEvents();
|
|
13544
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
13545
|
-
}
|
|
13546
|
-
async appendDelivery(result) {
|
|
13547
|
-
await this.init();
|
|
13548
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
13549
|
-
deliveries.push(result);
|
|
13550
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
13551
|
-
return result;
|
|
13552
|
-
}
|
|
13553
|
-
async listDeliveries() {
|
|
13554
|
-
await this.init();
|
|
13555
|
-
return this.readJson(this.deliveriesPath, []);
|
|
13556
|
-
}
|
|
13557
|
-
async exportData() {
|
|
13558
|
-
return {
|
|
13559
|
-
channels: await this.listChannels(),
|
|
13560
|
-
events: await this.listEvents(),
|
|
13561
|
-
deliveries: await this.listDeliveries()
|
|
13562
|
-
};
|
|
13563
|
-
}
|
|
13564
|
-
async ensureArrayFile(path) {
|
|
13565
|
-
if (!existsSync7(path)) {
|
|
13566
|
-
await writeFile(path, `[]
|
|
13567
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
13568
|
-
}
|
|
13569
|
-
await chmod(path, 384).catch(() => {
|
|
13570
|
-
return;
|
|
13571
|
-
});
|
|
13572
|
-
}
|
|
13573
|
-
async readJson(path, fallback) {
|
|
13574
|
-
try {
|
|
13575
|
-
const raw = await readFile(path, "utf-8");
|
|
13576
|
-
if (!raw.trim())
|
|
13577
|
-
return fallback;
|
|
13578
|
-
return JSON.parse(raw);
|
|
13579
|
-
} catch (error) {
|
|
13580
|
-
if (error.code === "ENOENT")
|
|
13581
|
-
return fallback;
|
|
13582
|
-
throw error;
|
|
13583
|
-
}
|
|
13584
|
-
}
|
|
13585
|
-
async writeJson(path, value) {
|
|
13586
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
13587
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
13588
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
13589
|
-
await rename(tempPath, path);
|
|
13590
|
-
await chmod(path, 384).catch(() => {
|
|
13591
|
-
return;
|
|
13592
|
-
});
|
|
13593
|
-
}
|
|
13594
|
-
}
|
|
13595
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
13596
|
-
function buildSignatureBase(timestamp, body) {
|
|
13597
|
-
return `${timestamp}.${body}`;
|
|
13598
|
-
}
|
|
13599
|
-
function signPayload(secret, timestamp, body) {
|
|
13600
|
-
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
13601
|
-
return `sha256=${digest}`;
|
|
13602
|
-
}
|
|
13603
|
-
function now() {
|
|
13604
|
-
return new Date().toISOString();
|
|
13605
|
-
}
|
|
13606
|
-
function truncate(value, max = 4096) {
|
|
13607
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
13608
|
-
}
|
|
13609
|
-
function buildWebhookRequest(event, channel) {
|
|
13610
|
-
if (!channel.webhook)
|
|
13611
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13612
|
-
const body = JSON.stringify(event);
|
|
13613
|
-
const timestamp = event.time;
|
|
13614
|
-
const headers = {
|
|
13615
|
-
"Content-Type": "application/json",
|
|
13616
|
-
"User-Agent": "@hasna/events",
|
|
13617
|
-
"X-Hasna-Event-Id": event.id,
|
|
13618
|
-
"X-Hasna-Event-Type": event.type,
|
|
13619
|
-
"X-Hasna-Timestamp": timestamp,
|
|
13620
|
-
...channel.webhook.headers
|
|
13621
|
-
};
|
|
13622
|
-
if (channel.webhook.secret) {
|
|
13623
|
-
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
13624
|
-
}
|
|
13625
|
-
return { body, headers };
|
|
13626
|
-
}
|
|
13627
|
-
async function dispatchWebhook2(event, channel, options = {}) {
|
|
13628
|
-
if (!channel.webhook)
|
|
13629
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13630
|
-
const startedAt = now();
|
|
13631
|
-
const { body, headers } = buildWebhookRequest(event, channel);
|
|
13632
|
-
const controller = new AbortController;
|
|
13633
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
13634
|
-
try {
|
|
13635
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
13636
|
-
method: "POST",
|
|
13637
|
-
headers,
|
|
13638
|
-
body,
|
|
13639
|
-
signal: controller.signal
|
|
13640
|
-
});
|
|
13641
|
-
const responseBody = truncate(await response.text());
|
|
13642
|
-
return {
|
|
13643
|
-
attempt: 1,
|
|
13644
|
-
status: response.ok ? "success" : "failed",
|
|
13645
|
-
startedAt,
|
|
13646
|
-
completedAt: now(),
|
|
13647
|
-
responseStatus: response.status,
|
|
13648
|
-
responseBody,
|
|
13649
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
13650
|
-
};
|
|
13651
|
-
} catch (error) {
|
|
13652
|
-
return {
|
|
13653
|
-
attempt: 1,
|
|
13654
|
-
status: "failed",
|
|
13655
|
-
startedAt,
|
|
13656
|
-
completedAt: now(),
|
|
13657
|
-
error: error instanceof Error ? error.message : String(error)
|
|
13658
|
-
};
|
|
13659
|
-
} finally {
|
|
13660
|
-
clearTimeout(timeout);
|
|
13661
|
-
}
|
|
13662
|
-
}
|
|
13663
|
-
async function dispatchCommand2(event, channel) {
|
|
13664
|
-
if (!channel.command)
|
|
13665
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
13666
|
-
const startedAt = now();
|
|
13667
|
-
const eventJson = JSON.stringify(event);
|
|
13668
|
-
const env = {
|
|
13669
|
-
...process.env,
|
|
13670
|
-
...channel.command.env,
|
|
13671
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
13672
|
-
HASNA_EVENT_ID: event.id,
|
|
13673
|
-
HASNA_EVENT_TYPE: event.type,
|
|
13674
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
13675
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
13676
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
13677
|
-
HASNA_EVENT_TIME: event.time,
|
|
13678
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
13679
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
13680
|
-
HASNA_EVENT_JSON: eventJson
|
|
13681
|
-
};
|
|
13682
|
-
return new Promise((resolve2) => {
|
|
13683
|
-
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
13684
|
-
cwd: channel.command.cwd,
|
|
13685
|
-
env,
|
|
13686
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
13687
|
-
});
|
|
13688
|
-
let stdout = "";
|
|
13689
|
-
let stderr = "";
|
|
13690
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
13691
|
-
child.stdin.end(eventJson);
|
|
13692
|
-
child.stdout.on("data", (chunk) => {
|
|
13693
|
-
stdout += chunk.toString();
|
|
13694
|
-
});
|
|
13695
|
-
child.stderr.on("data", (chunk) => {
|
|
13696
|
-
stderr += chunk.toString();
|
|
13697
|
-
});
|
|
13698
|
-
child.on("error", (error) => {
|
|
13699
|
-
clearTimeout(timeout);
|
|
13700
|
-
resolve2({
|
|
13701
|
-
attempt: 1,
|
|
13702
|
-
status: "failed",
|
|
13703
|
-
startedAt,
|
|
13704
|
-
completedAt: now(),
|
|
13705
|
-
stdout: truncate(stdout),
|
|
13706
|
-
stderr: truncate(stderr),
|
|
13707
|
-
error: error.message
|
|
13708
|
-
});
|
|
13709
|
-
});
|
|
13710
|
-
child.on("close", (code, signal) => {
|
|
13711
|
-
clearTimeout(timeout);
|
|
13712
|
-
const success = code === 0;
|
|
13713
|
-
resolve2({
|
|
13714
|
-
attempt: 1,
|
|
13715
|
-
status: success ? "success" : "failed",
|
|
13716
|
-
startedAt,
|
|
13717
|
-
completedAt: now(),
|
|
13718
|
-
stdout: truncate(stdout),
|
|
13719
|
-
stderr: truncate(stderr),
|
|
13720
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
13721
|
-
});
|
|
13722
|
-
});
|
|
13723
|
-
});
|
|
13724
|
-
}
|
|
13725
|
-
async function dispatchChannel2(event, channel, options = {}) {
|
|
13726
|
-
if (channel.transport === "webhook")
|
|
13727
|
-
return dispatchWebhook2(event, channel, options);
|
|
13728
|
-
if (channel.transport === "command")
|
|
13729
|
-
return dispatchCommand2(event, channel);
|
|
13730
|
-
return {
|
|
13731
|
-
attempt: 1,
|
|
13732
|
-
status: "skipped",
|
|
13733
|
-
startedAt: now(),
|
|
13734
|
-
completedAt: now(),
|
|
13735
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
13736
|
-
};
|
|
13737
|
-
}
|
|
13738
|
-
function createDeliveryResult(event, channel, attempts) {
|
|
13739
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
13740
|
-
return {
|
|
13741
|
-
id: randomUUID(),
|
|
13742
|
-
eventId: event.id,
|
|
13743
|
-
channelId: channel.id,
|
|
13744
|
-
transport: channel.transport,
|
|
13745
|
-
status,
|
|
13746
|
-
attempts,
|
|
13747
|
-
createdAt: attempts[0]?.startedAt ?? now(),
|
|
13748
|
-
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
13749
|
-
};
|
|
13750
|
-
}
|
|
13751
|
-
function createEvent(input) {
|
|
13752
|
-
return {
|
|
13753
|
-
id: input.id ?? randomUUID2(),
|
|
13754
|
-
source: input.source,
|
|
13755
|
-
type: input.type,
|
|
13756
|
-
time: normalizeTime(input.time),
|
|
13757
|
-
subject: input.subject,
|
|
13758
|
-
severity: input.severity ?? "info",
|
|
13759
|
-
data: input.data ?? {},
|
|
13760
|
-
message: input.message,
|
|
13761
|
-
dedupeKey: input.dedupeKey,
|
|
13762
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
13763
|
-
metadata: input.metadata ?? {}
|
|
13764
|
-
};
|
|
13765
|
-
}
|
|
13766
|
-
|
|
13767
|
-
class EventsClient {
|
|
13768
|
-
store;
|
|
13769
|
-
redactors;
|
|
13770
|
-
transportOptions;
|
|
13771
|
-
constructor(options = {}) {
|
|
13772
|
-
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
13773
|
-
this.redactors = options.redactors ?? [];
|
|
13774
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
13775
|
-
}
|
|
13776
|
-
async addChannel(input) {
|
|
13777
|
-
const timestamp = new Date().toISOString();
|
|
13778
|
-
return this.store.addChannel({
|
|
13779
|
-
...input,
|
|
13780
|
-
createdAt: input.createdAt ?? timestamp,
|
|
13781
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
13782
|
-
});
|
|
13783
|
-
}
|
|
13784
|
-
async listChannels() {
|
|
13785
|
-
return this.store.listChannels();
|
|
13786
|
-
}
|
|
13787
|
-
async removeChannel(id) {
|
|
13788
|
-
return this.store.removeChannel(id);
|
|
13789
|
-
}
|
|
13790
|
-
async emit(input, options = {}) {
|
|
13791
|
-
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
13792
|
-
if (options.dedupe !== false) {
|
|
13793
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
13794
|
-
if (existing) {
|
|
13795
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
13796
|
-
}
|
|
13797
|
-
}
|
|
13798
|
-
await this.store.appendEvent(event);
|
|
13799
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
13800
|
-
return { event, deliveries, deduped: false };
|
|
13801
|
-
}
|
|
13802
|
-
async listEvents() {
|
|
13803
|
-
return this.store.listEvents();
|
|
13804
|
-
}
|
|
13805
|
-
async listDeliveries() {
|
|
13806
|
-
return this.store.listDeliveries();
|
|
13807
|
-
}
|
|
13808
|
-
async deliver(event) {
|
|
13809
|
-
const channels = await this.store.listChannels();
|
|
13810
|
-
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
13811
|
-
const deliveries = [];
|
|
13812
|
-
for (const channel of selected) {
|
|
13813
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13814
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13815
|
-
await this.store.appendDelivery(result);
|
|
13816
|
-
deliveries.push(result);
|
|
13817
|
-
}
|
|
13818
|
-
return deliveries;
|
|
13819
|
-
}
|
|
13820
|
-
async testChannel(id, input = {}) {
|
|
13821
|
-
const channel = await this.store.getChannel(id);
|
|
13822
|
-
if (!channel)
|
|
13823
|
-
throw new Error(`Channel not found: ${id}`);
|
|
13824
|
-
const event = createEvent({
|
|
13825
|
-
source: input.source ?? "hasna.events",
|
|
13826
|
-
type: input.type ?? "events.test",
|
|
13827
|
-
subject: input.subject ?? id,
|
|
13828
|
-
severity: input.severity ?? "info",
|
|
13829
|
-
data: input.data ?? { test: true },
|
|
13830
|
-
message: input.message ?? "Hasna events test delivery",
|
|
13831
|
-
dedupeKey: input.dedupeKey,
|
|
13832
|
-
schemaVersion: input.schemaVersion,
|
|
13833
|
-
metadata: input.metadata,
|
|
13834
|
-
time: input.time,
|
|
13835
|
-
id: input.id
|
|
13836
|
-
});
|
|
13837
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13838
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13839
|
-
await this.store.appendDelivery(result);
|
|
13840
|
-
return result;
|
|
13841
|
-
}
|
|
13842
|
-
async replay(options = {}) {
|
|
13843
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
13844
|
-
if (options.eventId && event.id !== options.eventId)
|
|
13845
|
-
return false;
|
|
13846
|
-
if (options.source && event.source !== options.source)
|
|
13847
|
-
return false;
|
|
13848
|
-
if (options.type && event.type !== options.type)
|
|
13849
|
-
return false;
|
|
13850
|
-
return true;
|
|
13851
|
-
});
|
|
13852
|
-
if (options.dryRun)
|
|
13853
|
-
return { events, deliveries: [] };
|
|
13854
|
-
const deliveries = [];
|
|
13855
|
-
for (const event of events) {
|
|
13856
|
-
deliveries.push(...await this.deliver(event));
|
|
13857
|
-
}
|
|
13858
|
-
return { events, deliveries };
|
|
13859
|
-
}
|
|
13860
|
-
async applyRedaction(event, channel) {
|
|
13861
|
-
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
13862
|
-
for (const redactor of this.redactors) {
|
|
13863
|
-
next = await redactor(next, channel);
|
|
13864
|
-
}
|
|
13865
|
-
return next;
|
|
13866
|
-
}
|
|
13867
|
-
async deliverWithRetry(event, channel) {
|
|
13868
|
-
const policy = normalizeRetryPolicy(channel.retry);
|
|
13869
|
-
const attempts = [];
|
|
13870
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
13871
|
-
const attempt = await dispatchChannel2(event, channel, this.transportOptions);
|
|
13872
|
-
attempt.attempt = index + 1;
|
|
13873
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
13874
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
13875
|
-
}
|
|
13876
|
-
attempts.push(attempt);
|
|
13877
|
-
if (attempt.status !== "failed")
|
|
13878
|
-
break;
|
|
13879
|
-
if (attempt.nextBackoffMs)
|
|
13880
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
13881
|
-
}
|
|
13882
|
-
return createDeliveryResult(event, channel, attempts);
|
|
13883
|
-
}
|
|
13884
|
-
}
|
|
13885
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
13886
|
-
if (paths.length === 0)
|
|
13887
|
-
return event;
|
|
13888
|
-
const copy = structuredClone(event);
|
|
13889
|
-
for (const path of paths) {
|
|
13890
|
-
setPath(copy, path, replacement);
|
|
13891
|
-
}
|
|
13892
|
-
return copy;
|
|
13893
|
-
}
|
|
13894
|
-
function sanitizeChannelForOutput(channel) {
|
|
13895
|
-
const copy = structuredClone(channel);
|
|
13896
|
-
if (copy.webhook?.secret)
|
|
13897
|
-
copy.webhook.secret = "[REDACTED]";
|
|
13898
|
-
if (copy.command?.env) {
|
|
13899
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
13900
|
-
}
|
|
13901
|
-
return copy;
|
|
13902
|
-
}
|
|
13903
|
-
function sanitizeChannelsForOutput(channels) {
|
|
13904
|
-
return channels.map(sanitizeChannelForOutput);
|
|
13905
|
-
}
|
|
13906
|
-
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
13907
|
-
return redactValue(event, replacement);
|
|
13908
|
-
}
|
|
13909
|
-
function shouldRedactKey(key) {
|
|
13910
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
13911
|
-
}
|
|
13912
|
-
function redactValue(value, replacement) {
|
|
13913
|
-
if (Array.isArray(value))
|
|
13914
|
-
return value.map((item) => redactValue(item, replacement));
|
|
13915
|
-
if (!value || typeof value !== "object")
|
|
13916
|
-
return value;
|
|
13917
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
13918
|
-
key,
|
|
13919
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
13920
|
-
]));
|
|
13921
|
-
}
|
|
13922
|
-
function setPath(input, path, replacement) {
|
|
13923
|
-
const parts = path.split(".");
|
|
13924
|
-
let cursor = input;
|
|
13925
|
-
for (const part of parts.slice(0, -1)) {
|
|
13926
|
-
const next = cursor[part];
|
|
13927
|
-
if (!next || typeof next !== "object")
|
|
13928
|
-
return;
|
|
13929
|
-
cursor = next;
|
|
13930
|
-
}
|
|
13931
|
-
const last = parts.at(-1);
|
|
13932
|
-
if (last && last in cursor)
|
|
13933
|
-
cursor[last] = replacement;
|
|
13934
|
-
}
|
|
13935
|
-
function normalizeTime(value) {
|
|
13936
|
-
if (!value)
|
|
13937
|
-
return new Date().toISOString();
|
|
13938
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
13939
|
-
}
|
|
13940
|
-
function normalizeRetryPolicy(policy) {
|
|
13941
|
-
return {
|
|
13942
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
13943
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
13944
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
13945
|
-
};
|
|
13946
|
-
}
|
|
13947
|
-
|
|
13948
|
-
// src/commands/runtime.ts
|
|
13443
|
+
import { EventsClient } from "@hasna/events";
|
|
13949
13444
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
13950
13445
|
const checkedAt = new Date().toISOString();
|
|
13951
13446
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -14030,6 +13525,9 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
14030
13525
|
};
|
|
14031
13526
|
return client.emit(input, { deliver });
|
|
14032
13527
|
}
|
|
13528
|
+
// src/commands/serve.ts
|
|
13529
|
+
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
13530
|
+
|
|
14033
13531
|
// src/commands/status.ts
|
|
14034
13532
|
function getStatus() {
|
|
14035
13533
|
const manifest = readManifest();
|
|
@@ -14255,7 +13753,7 @@ function jsonError(message, status = 400) {
|
|
|
14255
13753
|
}
|
|
14256
13754
|
function startDashboardServer(options = {}) {
|
|
14257
13755
|
const info = getServeInfo(options);
|
|
14258
|
-
const events = new
|
|
13756
|
+
const events = new EventsClient2;
|
|
14259
13757
|
return Bun.serve({
|
|
14260
13758
|
hostname: info.host,
|
|
14261
13759
|
port: info.port,
|
|
@@ -14409,7 +13907,7 @@ function runSelfTest() {
|
|
|
14409
13907
|
};
|
|
14410
13908
|
}
|
|
14411
13909
|
// src/commands/setup.ts
|
|
14412
|
-
import { homedir as
|
|
13910
|
+
import { homedir as homedir4 } from "os";
|
|
14413
13911
|
function quote3(value) {
|
|
14414
13912
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14415
13913
|
}
|
|
@@ -14476,10 +13974,13 @@ function buildSetupPlan(machineId) {
|
|
|
14476
13974
|
const manifest = readManifest();
|
|
14477
13975
|
const currentMachineId = getLocalMachineId();
|
|
14478
13976
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
13977
|
+
if (machineId && !selected) {
|
|
13978
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
13979
|
+
}
|
|
14479
13980
|
const target = selected || {
|
|
14480
13981
|
id: currentMachineId,
|
|
14481
13982
|
platform: "linux",
|
|
14482
|
-
workspacePath: `${
|
|
13983
|
+
workspacePath: `${homedir4()}/workspace`
|
|
14483
13984
|
};
|
|
14484
13985
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
14485
13986
|
return {
|
|
@@ -14523,7 +14024,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
14523
14024
|
return summary;
|
|
14524
14025
|
}
|
|
14525
14026
|
// src/commands/screen.ts
|
|
14526
|
-
var
|
|
14027
|
+
var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
|
|
14028
|
+
var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
|
|
14527
14029
|
function shellQuote7(value) {
|
|
14528
14030
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
14529
14031
|
}
|
|
@@ -14547,7 +14049,8 @@ function splitTarget(target) {
|
|
|
14547
14049
|
return [target.slice(0, at), target.slice(at + 1)];
|
|
14548
14050
|
}
|
|
14549
14051
|
function defaultScreenPasswordSecretKey(machineId) {
|
|
14550
|
-
|
|
14052
|
+
const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
|
|
14053
|
+
return `${namespace}/screen-${machineId}-vnc-password`;
|
|
14551
14054
|
}
|
|
14552
14055
|
function resolveScreenTarget(machineId, options = {}) {
|
|
14553
14056
|
const resolved = resolveMachineRoute(machineId, options);
|
|
@@ -14644,8 +14147,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
14644
14147
|
};
|
|
14645
14148
|
}
|
|
14646
14149
|
// src/commands/sync.ts
|
|
14647
|
-
import { existsSync as
|
|
14648
|
-
import { homedir as
|
|
14150
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
14151
|
+
import { homedir as homedir5 } from "os";
|
|
14649
14152
|
function quote4(value) {
|
|
14650
14153
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14651
14154
|
}
|
|
@@ -14697,8 +14200,8 @@ function detectFileActions(machine) {
|
|
|
14697
14200
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
14698
14201
|
}
|
|
14699
14202
|
return (machine.files || []).map((file, index) => {
|
|
14700
|
-
const sourceExists =
|
|
14701
|
-
const targetExists =
|
|
14203
|
+
const sourceExists = existsSync7(file.source);
|
|
14204
|
+
const targetExists = existsSync7(file.target);
|
|
14702
14205
|
let status = "missing";
|
|
14703
14206
|
if (sourceExists && targetExists) {
|
|
14704
14207
|
if (file.mode === "symlink") {
|
|
@@ -14723,10 +14226,13 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
14723
14226
|
const manifest = readManifest();
|
|
14724
14227
|
const currentMachineId = getLocalMachineId();
|
|
14725
14228
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
14229
|
+
if (machineId && !selected) {
|
|
14230
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
14231
|
+
}
|
|
14726
14232
|
const target = selected || {
|
|
14727
14233
|
id: currentMachineId,
|
|
14728
14234
|
platform: "linux",
|
|
14729
|
-
workspacePath: `${
|
|
14235
|
+
workspacePath: `${homedir5()}/workspace`
|
|
14730
14236
|
};
|
|
14731
14237
|
const actions = [
|
|
14732
14238
|
...detectPackageActions(target, runner),
|
|
@@ -14946,6 +14452,9 @@ function repairWorkspaceManifestMappings(options) {
|
|
|
14946
14452
|
warnings
|
|
14947
14453
|
};
|
|
14948
14454
|
}
|
|
14455
|
+
// src/mcp/server.ts
|
|
14456
|
+
import { EventsClient as EventsClient3, sanitizeChannelForOutput, sanitizeChannelsForOutput as sanitizeChannelsForOutput2 } from "@hasna/events";
|
|
14457
|
+
|
|
14949
14458
|
// node_modules/zod/v4/core/core.js
|
|
14950
14459
|
var NEVER2 = Object.freeze({
|
|
14951
14460
|
status: "aborted"
|
|
@@ -23918,7 +23427,7 @@ function buildServer(version2 = getPackageVersion()) {
|
|
|
23918
23427
|
}
|
|
23919
23428
|
function createMcpServer(version2) {
|
|
23920
23429
|
const server = new McpServer({ name: "machines", version: version2 });
|
|
23921
|
-
const events = new
|
|
23430
|
+
const events = new EventsClient3;
|
|
23922
23431
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
23923
23432
|
content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
|
|
23924
23433
|
}));
|
|
@@ -24072,7 +23581,7 @@ function createMcpServer(version2) {
|
|
|
24072
23581
|
}));
|
|
24073
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) }] }));
|
|
24074
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) }] }));
|
|
24075
|
-
server.tool("machines_webhooks_add", "Add or replace a shared
|
|
23584
|
+
server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
|
|
24076
23585
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
24077
23586
|
url: exports_external.string().url().describe("Webhook URL"),
|
|
24078
23587
|
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
@@ -24080,26 +23589,26 @@ function createMcpServer(version2) {
|
|
|
24080
23589
|
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
24081
23590
|
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
24082
23591
|
}, async ({ channel_id, url, event_type, source, secret, enabled }) => {
|
|
24083
|
-
const
|
|
23592
|
+
const now = new Date().toISOString();
|
|
24084
23593
|
const channel = await events.addChannel({
|
|
24085
23594
|
id: channel_id,
|
|
24086
23595
|
enabled: enabled ?? true,
|
|
24087
23596
|
transport: "webhook",
|
|
24088
23597
|
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
24089
23598
|
webhook: { url, secret },
|
|
24090
|
-
createdAt:
|
|
24091
|
-
updatedAt:
|
|
23599
|
+
createdAt: now,
|
|
23600
|
+
updatedAt: now
|
|
24092
23601
|
});
|
|
24093
23602
|
return { content: [{ type: "text", text: JSON.stringify(sanitizeChannelForOutput(channel), null, 2) }] };
|
|
24094
23603
|
});
|
|
24095
|
-
server.tool("machines_webhooks_list", "List shared
|
|
24096
|
-
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) }]
|
|
24097
23606
|
}));
|
|
24098
|
-
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 }) => ({
|
|
24099
23608
|
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
24100
23609
|
}));
|
|
24101
|
-
server.tool("machines_webhooks_remove", "Remove a shared
|
|
24102
|
-
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.", {
|
|
24103
23612
|
event_type: exports_external.string().describe("Event type"),
|
|
24104
23613
|
subject: exports_external.string().optional().describe("Event subject"),
|
|
24105
23614
|
severity: exports_external.enum(["debug", "info", "notice", "warning", "error", "critical"]).optional().describe("Event severity"),
|
|
@@ -24120,10 +23629,10 @@ function createMcpServer(version2) {
|
|
|
24120
23629
|
dedupeKey: dedupe_key
|
|
24121
23630
|
}, { deliver: deliver !== false }), null, 2) }]
|
|
24122
23631
|
}));
|
|
24123
|
-
server.tool("machines_events_list", "List shared
|
|
23632
|
+
server.tool("machines_events_list", "List shared events.", {}, async () => ({
|
|
24124
23633
|
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
24125
23634
|
}));
|
|
24126
|
-
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 }) => ({
|
|
24127
23636
|
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
24128
23637
|
}));
|
|
24129
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) }] }));
|
|
@@ -24254,6 +23763,7 @@ export {
|
|
|
24254
23763
|
STORAGE_TABLES,
|
|
24255
23764
|
STORAGE_MODE_ENV,
|
|
24256
23765
|
STORAGE_DATABASE_ENV,
|
|
23766
|
+
SCREEN_SECRET_NAMESPACE_ENV,
|
|
24257
23767
|
MACHINE_MCP_TOOL_NAMES,
|
|
24258
23768
|
MACHINES_STORAGE_TABLES,
|
|
24259
23769
|
MACHINES_STORAGE_MODE_FALLBACK_ENV,
|