@hasna/machines 0.0.27 → 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/dist/cli/index.js +1374 -113
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/index.js +654 -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/mcp/index.js
CHANGED
|
@@ -40,6 +40,526 @@ function getPackageVersion() {
|
|
|
40
40
|
import { createServer } from "http";
|
|
41
41
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
42
42
|
|
|
43
|
+
// node_modules/@hasna/events/dist/index.js
|
|
44
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
45
|
+
import { existsSync as existsSync2 } from "fs";
|
|
46
|
+
import { homedir } from "os";
|
|
47
|
+
import { join as join2 } from "path";
|
|
48
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
49
|
+
import { randomUUID } from "crypto";
|
|
50
|
+
import { spawn } from "child_process";
|
|
51
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
52
|
+
function getPathValue(input, path) {
|
|
53
|
+
return path.split(".").reduce((value, part) => {
|
|
54
|
+
if (value && typeof value === "object" && part in value) {
|
|
55
|
+
return value[part];
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}, input);
|
|
59
|
+
}
|
|
60
|
+
function wildcardToRegExp(pattern) {
|
|
61
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
62
|
+
return new RegExp(`^${escaped}$`);
|
|
63
|
+
}
|
|
64
|
+
function matchString(value, matcher) {
|
|
65
|
+
if (matcher === undefined)
|
|
66
|
+
return true;
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return false;
|
|
69
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
70
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
71
|
+
}
|
|
72
|
+
function matchRecord(input, matcher) {
|
|
73
|
+
if (!matcher)
|
|
74
|
+
return true;
|
|
75
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
76
|
+
const actual = getPathValue(input, path);
|
|
77
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
78
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
79
|
+
}
|
|
80
|
+
return actual === expected;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function eventMatchesFilter(event, filter) {
|
|
84
|
+
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);
|
|
85
|
+
}
|
|
86
|
+
function channelMatchesEvent(channel, event) {
|
|
87
|
+
if (!channel.enabled)
|
|
88
|
+
return false;
|
|
89
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
90
|
+
return true;
|
|
91
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
92
|
+
}
|
|
93
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
94
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
95
|
+
function getEventsDataDir(override) {
|
|
96
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join2(homedir(), ".hasna", "events");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class JsonEventsStore {
|
|
100
|
+
dataDir;
|
|
101
|
+
channelsPath;
|
|
102
|
+
eventsPath;
|
|
103
|
+
deliveriesPath;
|
|
104
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
105
|
+
this.dataDir = dataDir;
|
|
106
|
+
this.channelsPath = join2(dataDir, "channels.json");
|
|
107
|
+
this.eventsPath = join2(dataDir, "events.json");
|
|
108
|
+
this.deliveriesPath = join2(dataDir, "deliveries.json");
|
|
109
|
+
}
|
|
110
|
+
async init() {
|
|
111
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
112
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
113
|
+
return;
|
|
114
|
+
});
|
|
115
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
116
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
117
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
118
|
+
}
|
|
119
|
+
async addChannel(channel) {
|
|
120
|
+
await this.init();
|
|
121
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
122
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
123
|
+
if (index >= 0) {
|
|
124
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
125
|
+
} else {
|
|
126
|
+
channels.push(channel);
|
|
127
|
+
}
|
|
128
|
+
await this.writeJson(this.channelsPath, channels);
|
|
129
|
+
return index >= 0 ? channels[index] : channel;
|
|
130
|
+
}
|
|
131
|
+
async listChannels() {
|
|
132
|
+
await this.init();
|
|
133
|
+
return this.readJson(this.channelsPath, []);
|
|
134
|
+
}
|
|
135
|
+
async getChannel(id) {
|
|
136
|
+
const channels = await this.listChannels();
|
|
137
|
+
return channels.find((channel) => channel.id === id);
|
|
138
|
+
}
|
|
139
|
+
async removeChannel(id) {
|
|
140
|
+
await this.init();
|
|
141
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
142
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
143
|
+
await this.writeJson(this.channelsPath, next);
|
|
144
|
+
return next.length !== channels.length;
|
|
145
|
+
}
|
|
146
|
+
async appendEvent(event) {
|
|
147
|
+
await this.init();
|
|
148
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
149
|
+
events.push(event);
|
|
150
|
+
await this.writeJson(this.eventsPath, events);
|
|
151
|
+
return event;
|
|
152
|
+
}
|
|
153
|
+
async listEvents() {
|
|
154
|
+
await this.init();
|
|
155
|
+
return this.readJson(this.eventsPath, []);
|
|
156
|
+
}
|
|
157
|
+
async findEventByIdentity(identity) {
|
|
158
|
+
const events = await this.listEvents();
|
|
159
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
160
|
+
}
|
|
161
|
+
async appendDelivery(result) {
|
|
162
|
+
await this.init();
|
|
163
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
164
|
+
deliveries.push(result);
|
|
165
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
async listDeliveries() {
|
|
169
|
+
await this.init();
|
|
170
|
+
return this.readJson(this.deliveriesPath, []);
|
|
171
|
+
}
|
|
172
|
+
async exportData() {
|
|
173
|
+
return {
|
|
174
|
+
channels: await this.listChannels(),
|
|
175
|
+
events: await this.listEvents(),
|
|
176
|
+
deliveries: await this.listDeliveries()
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async ensureArrayFile(path) {
|
|
180
|
+
if (!existsSync2(path)) {
|
|
181
|
+
await writeFile(path, `[]
|
|
182
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
183
|
+
}
|
|
184
|
+
await chmod(path, 384).catch(() => {
|
|
185
|
+
return;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async readJson(path, fallback) {
|
|
189
|
+
try {
|
|
190
|
+
const raw = await readFile(path, "utf-8");
|
|
191
|
+
if (!raw.trim())
|
|
192
|
+
return fallback;
|
|
193
|
+
return JSON.parse(raw);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error.code === "ENOENT")
|
|
196
|
+
return fallback;
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async writeJson(path, value) {
|
|
201
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
202
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
203
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
204
|
+
await rename(tempPath, path);
|
|
205
|
+
await chmod(path, 384).catch(() => {
|
|
206
|
+
return;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
211
|
+
function buildSignatureBase(timestamp, body) {
|
|
212
|
+
return `${timestamp}.${body}`;
|
|
213
|
+
}
|
|
214
|
+
function signPayload(secret, timestamp, body) {
|
|
215
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
216
|
+
return `sha256=${digest}`;
|
|
217
|
+
}
|
|
218
|
+
function now() {
|
|
219
|
+
return new Date().toISOString();
|
|
220
|
+
}
|
|
221
|
+
function truncate(value, max = 4096) {
|
|
222
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
223
|
+
}
|
|
224
|
+
function buildWebhookRequest(event, channel) {
|
|
225
|
+
if (!channel.webhook)
|
|
226
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
227
|
+
const body = JSON.stringify(event);
|
|
228
|
+
const timestamp = event.time;
|
|
229
|
+
const headers = {
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
"User-Agent": "@hasna/events",
|
|
232
|
+
"X-Hasna-Event-Id": event.id,
|
|
233
|
+
"X-Hasna-Event-Type": event.type,
|
|
234
|
+
"X-Hasna-Timestamp": timestamp,
|
|
235
|
+
...channel.webhook.headers
|
|
236
|
+
};
|
|
237
|
+
if (channel.webhook.secret) {
|
|
238
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
239
|
+
}
|
|
240
|
+
return { body, headers };
|
|
241
|
+
}
|
|
242
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
243
|
+
if (!channel.webhook)
|
|
244
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
245
|
+
const startedAt = now();
|
|
246
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
247
|
+
const controller = new AbortController;
|
|
248
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
249
|
+
try {
|
|
250
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers,
|
|
253
|
+
body,
|
|
254
|
+
signal: controller.signal
|
|
255
|
+
});
|
|
256
|
+
const responseBody = truncate(await response.text());
|
|
257
|
+
return {
|
|
258
|
+
attempt: 1,
|
|
259
|
+
status: response.ok ? "success" : "failed",
|
|
260
|
+
startedAt,
|
|
261
|
+
completedAt: now(),
|
|
262
|
+
responseStatus: response.status,
|
|
263
|
+
responseBody,
|
|
264
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return {
|
|
268
|
+
attempt: 1,
|
|
269
|
+
status: "failed",
|
|
270
|
+
startedAt,
|
|
271
|
+
completedAt: now(),
|
|
272
|
+
error: error instanceof Error ? error.message : String(error)
|
|
273
|
+
};
|
|
274
|
+
} finally {
|
|
275
|
+
clearTimeout(timeout);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function dispatchCommand(event, channel) {
|
|
279
|
+
if (!channel.command)
|
|
280
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
281
|
+
const startedAt = now();
|
|
282
|
+
const eventJson = JSON.stringify(event);
|
|
283
|
+
const env = {
|
|
284
|
+
...process.env,
|
|
285
|
+
...channel.command.env,
|
|
286
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
287
|
+
HASNA_EVENT_ID: event.id,
|
|
288
|
+
HASNA_EVENT_TYPE: event.type,
|
|
289
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
290
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
291
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
292
|
+
HASNA_EVENT_TIME: event.time,
|
|
293
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
294
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
295
|
+
HASNA_EVENT_JSON: eventJson
|
|
296
|
+
};
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
299
|
+
cwd: channel.command.cwd,
|
|
300
|
+
env,
|
|
301
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
302
|
+
});
|
|
303
|
+
let stdout = "";
|
|
304
|
+
let stderr = "";
|
|
305
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
306
|
+
child.stdin.end(eventJson);
|
|
307
|
+
child.stdout.on("data", (chunk) => {
|
|
308
|
+
stdout += chunk.toString();
|
|
309
|
+
});
|
|
310
|
+
child.stderr.on("data", (chunk) => {
|
|
311
|
+
stderr += chunk.toString();
|
|
312
|
+
});
|
|
313
|
+
child.on("error", (error) => {
|
|
314
|
+
clearTimeout(timeout);
|
|
315
|
+
resolve({
|
|
316
|
+
attempt: 1,
|
|
317
|
+
status: "failed",
|
|
318
|
+
startedAt,
|
|
319
|
+
completedAt: now(),
|
|
320
|
+
stdout: truncate(stdout),
|
|
321
|
+
stderr: truncate(stderr),
|
|
322
|
+
error: error.message
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
child.on("close", (code, signal) => {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
const success = code === 0;
|
|
328
|
+
resolve({
|
|
329
|
+
attempt: 1,
|
|
330
|
+
status: success ? "success" : "failed",
|
|
331
|
+
startedAt,
|
|
332
|
+
completedAt: now(),
|
|
333
|
+
stdout: truncate(stdout),
|
|
334
|
+
stderr: truncate(stderr),
|
|
335
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
341
|
+
if (channel.transport === "webhook")
|
|
342
|
+
return dispatchWebhook(event, channel, options);
|
|
343
|
+
if (channel.transport === "command")
|
|
344
|
+
return dispatchCommand(event, channel);
|
|
345
|
+
return {
|
|
346
|
+
attempt: 1,
|
|
347
|
+
status: "skipped",
|
|
348
|
+
startedAt: now(),
|
|
349
|
+
completedAt: now(),
|
|
350
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
354
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
355
|
+
return {
|
|
356
|
+
id: randomUUID(),
|
|
357
|
+
eventId: event.id,
|
|
358
|
+
channelId: channel.id,
|
|
359
|
+
transport: channel.transport,
|
|
360
|
+
status,
|
|
361
|
+
attempts,
|
|
362
|
+
createdAt: attempts[0]?.startedAt ?? now(),
|
|
363
|
+
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function createEvent(input) {
|
|
367
|
+
return {
|
|
368
|
+
id: input.id ?? randomUUID2(),
|
|
369
|
+
source: input.source,
|
|
370
|
+
type: input.type,
|
|
371
|
+
time: normalizeTime(input.time),
|
|
372
|
+
subject: input.subject,
|
|
373
|
+
severity: input.severity ?? "info",
|
|
374
|
+
data: input.data ?? {},
|
|
375
|
+
message: input.message,
|
|
376
|
+
dedupeKey: input.dedupeKey,
|
|
377
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
378
|
+
metadata: input.metadata ?? {}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
class EventsClient {
|
|
383
|
+
store;
|
|
384
|
+
redactors;
|
|
385
|
+
transportOptions;
|
|
386
|
+
constructor(options = {}) {
|
|
387
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
388
|
+
this.redactors = options.redactors ?? [];
|
|
389
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
390
|
+
}
|
|
391
|
+
async addChannel(input) {
|
|
392
|
+
const timestamp = new Date().toISOString();
|
|
393
|
+
return this.store.addChannel({
|
|
394
|
+
...input,
|
|
395
|
+
createdAt: input.createdAt ?? timestamp,
|
|
396
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
async listChannels() {
|
|
400
|
+
return this.store.listChannels();
|
|
401
|
+
}
|
|
402
|
+
async removeChannel(id) {
|
|
403
|
+
return this.store.removeChannel(id);
|
|
404
|
+
}
|
|
405
|
+
async emit(input, options = {}) {
|
|
406
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
407
|
+
if (options.dedupe !== false) {
|
|
408
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
409
|
+
if (existing) {
|
|
410
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
await this.store.appendEvent(event);
|
|
414
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
415
|
+
return { event, deliveries, deduped: false };
|
|
416
|
+
}
|
|
417
|
+
async listEvents() {
|
|
418
|
+
return this.store.listEvents();
|
|
419
|
+
}
|
|
420
|
+
async listDeliveries() {
|
|
421
|
+
return this.store.listDeliveries();
|
|
422
|
+
}
|
|
423
|
+
async deliver(event) {
|
|
424
|
+
const channels = await this.store.listChannels();
|
|
425
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
426
|
+
const deliveries = [];
|
|
427
|
+
for (const channel of selected) {
|
|
428
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
429
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
430
|
+
await this.store.appendDelivery(result);
|
|
431
|
+
deliveries.push(result);
|
|
432
|
+
}
|
|
433
|
+
return deliveries;
|
|
434
|
+
}
|
|
435
|
+
async testChannel(id, input = {}) {
|
|
436
|
+
const channel = await this.store.getChannel(id);
|
|
437
|
+
if (!channel)
|
|
438
|
+
throw new Error(`Channel not found: ${id}`);
|
|
439
|
+
const event = createEvent({
|
|
440
|
+
source: input.source ?? "hasna.events",
|
|
441
|
+
type: input.type ?? "events.test",
|
|
442
|
+
subject: input.subject ?? id,
|
|
443
|
+
severity: input.severity ?? "info",
|
|
444
|
+
data: input.data ?? { test: true },
|
|
445
|
+
message: input.message ?? "Hasna events test delivery",
|
|
446
|
+
dedupeKey: input.dedupeKey,
|
|
447
|
+
schemaVersion: input.schemaVersion,
|
|
448
|
+
metadata: input.metadata,
|
|
449
|
+
time: input.time,
|
|
450
|
+
id: input.id
|
|
451
|
+
});
|
|
452
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
453
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
454
|
+
await this.store.appendDelivery(result);
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
async replay(options = {}) {
|
|
458
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
459
|
+
if (options.eventId && event.id !== options.eventId)
|
|
460
|
+
return false;
|
|
461
|
+
if (options.source && event.source !== options.source)
|
|
462
|
+
return false;
|
|
463
|
+
if (options.type && event.type !== options.type)
|
|
464
|
+
return false;
|
|
465
|
+
return true;
|
|
466
|
+
});
|
|
467
|
+
if (options.dryRun)
|
|
468
|
+
return { events, deliveries: [] };
|
|
469
|
+
const deliveries = [];
|
|
470
|
+
for (const event of events) {
|
|
471
|
+
deliveries.push(...await this.deliver(event));
|
|
472
|
+
}
|
|
473
|
+
return { events, deliveries };
|
|
474
|
+
}
|
|
475
|
+
async applyRedaction(event, channel) {
|
|
476
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
477
|
+
for (const redactor of this.redactors) {
|
|
478
|
+
next = await redactor(next, channel);
|
|
479
|
+
}
|
|
480
|
+
return next;
|
|
481
|
+
}
|
|
482
|
+
async deliverWithRetry(event, channel) {
|
|
483
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
484
|
+
const attempts = [];
|
|
485
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
486
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
487
|
+
attempt.attempt = index + 1;
|
|
488
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
489
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
490
|
+
}
|
|
491
|
+
attempts.push(attempt);
|
|
492
|
+
if (attempt.status !== "failed")
|
|
493
|
+
break;
|
|
494
|
+
if (attempt.nextBackoffMs)
|
|
495
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
496
|
+
}
|
|
497
|
+
return createDeliveryResult(event, channel, attempts);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
501
|
+
if (paths.length === 0)
|
|
502
|
+
return event;
|
|
503
|
+
const copy = structuredClone(event);
|
|
504
|
+
for (const path of paths) {
|
|
505
|
+
setPath(copy, path, replacement);
|
|
506
|
+
}
|
|
507
|
+
return copy;
|
|
508
|
+
}
|
|
509
|
+
function sanitizeChannelForOutput(channel) {
|
|
510
|
+
const copy = structuredClone(channel);
|
|
511
|
+
if (copy.webhook?.secret)
|
|
512
|
+
copy.webhook.secret = "[REDACTED]";
|
|
513
|
+
if (copy.command?.env) {
|
|
514
|
+
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
515
|
+
}
|
|
516
|
+
return copy;
|
|
517
|
+
}
|
|
518
|
+
function sanitizeChannelsForOutput(channels) {
|
|
519
|
+
return channels.map(sanitizeChannelForOutput);
|
|
520
|
+
}
|
|
521
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
522
|
+
return redactValue(event, replacement);
|
|
523
|
+
}
|
|
524
|
+
function shouldRedactKey(key) {
|
|
525
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
526
|
+
}
|
|
527
|
+
function redactValue(value, replacement) {
|
|
528
|
+
if (Array.isArray(value))
|
|
529
|
+
return value.map((item) => redactValue(item, replacement));
|
|
530
|
+
if (!value || typeof value !== "object")
|
|
531
|
+
return value;
|
|
532
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
533
|
+
key,
|
|
534
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
535
|
+
]));
|
|
536
|
+
}
|
|
537
|
+
function setPath(input, path, replacement) {
|
|
538
|
+
const parts = path.split(".");
|
|
539
|
+
let cursor = input;
|
|
540
|
+
for (const part of parts.slice(0, -1)) {
|
|
541
|
+
const next = cursor[part];
|
|
542
|
+
if (!next || typeof next !== "object")
|
|
543
|
+
return;
|
|
544
|
+
cursor = next;
|
|
545
|
+
}
|
|
546
|
+
const last = parts.at(-1);
|
|
547
|
+
if (last && last in cursor)
|
|
548
|
+
cursor[last] = replacement;
|
|
549
|
+
}
|
|
550
|
+
function normalizeTime(value) {
|
|
551
|
+
if (!value)
|
|
552
|
+
return new Date().toISOString();
|
|
553
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
554
|
+
}
|
|
555
|
+
function normalizeRetryPolicy(policy) {
|
|
556
|
+
return {
|
|
557
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
558
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
559
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
43
563
|
// src/mcp/server.ts
|
|
44
564
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
45
565
|
|
|
@@ -4017,8 +4537,8 @@ var coerce = {
|
|
|
4017
4537
|
};
|
|
4018
4538
|
var NEVER = INVALID;
|
|
4019
4539
|
// src/commands/backup.ts
|
|
4020
|
-
import { homedir, hostname } from "os";
|
|
4021
|
-
import { join as
|
|
4540
|
+
import { homedir as homedir2, hostname } from "os";
|
|
4541
|
+
import { join as join3 } from "path";
|
|
4022
4542
|
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
4023
4543
|
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
4024
4544
|
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
@@ -4066,16 +4586,16 @@ function resolveBackupTarget(options = {}) {
|
|
|
4066
4586
|
};
|
|
4067
4587
|
}
|
|
4068
4588
|
function defaultBackupSources() {
|
|
4069
|
-
const home =
|
|
4589
|
+
const home = homedir2();
|
|
4070
4590
|
return [
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4591
|
+
join3(home, ".hasna"),
|
|
4592
|
+
join3(home, ".ssh"),
|
|
4593
|
+
join3(home, ".secrets")
|
|
4074
4594
|
];
|
|
4075
4595
|
}
|
|
4076
4596
|
function buildBackupPlan(bucket, prefix) {
|
|
4077
4597
|
const target = resolveBackupTarget({ bucket, prefix });
|
|
4078
|
-
const archivePath =
|
|
4598
|
+
const archivePath = join3(homedir2(), ".hasna", "machines", "backup.tgz");
|
|
4079
4599
|
const sources = defaultBackupSources();
|
|
4080
4600
|
const steps = [
|
|
4081
4601
|
{
|
|
@@ -4126,33 +4646,33 @@ function runBackup(bucket, prefix, options = {}) {
|
|
|
4126
4646
|
}
|
|
4127
4647
|
|
|
4128
4648
|
// src/manifests.ts
|
|
4129
|
-
import { existsSync as
|
|
4130
|
-
import { arch, homedir as
|
|
4649
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4650
|
+
import { arch, homedir as homedir3, hostname as hostname2, platform, userInfo } from "os";
|
|
4131
4651
|
import { dirname as dirname3 } from "path";
|
|
4132
4652
|
|
|
4133
4653
|
// src/paths.ts
|
|
4134
|
-
import { existsSync as
|
|
4135
|
-
import { dirname as dirname2, join as
|
|
4654
|
+
import { existsSync as existsSync3, mkdirSync } from "fs";
|
|
4655
|
+
import { dirname as dirname2, join as join4, resolve } from "path";
|
|
4136
4656
|
function homeDir() {
|
|
4137
4657
|
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4138
4658
|
}
|
|
4139
4659
|
function getDataDir() {
|
|
4140
|
-
return process.env["HASNA_MACHINES_DIR"] ||
|
|
4660
|
+
return process.env["HASNA_MACHINES_DIR"] || join4(homeDir(), ".hasna", "machines");
|
|
4141
4661
|
}
|
|
4142
4662
|
function getDbPath() {
|
|
4143
|
-
return process.env["HASNA_MACHINES_DB_PATH"] ||
|
|
4663
|
+
return process.env["HASNA_MACHINES_DB_PATH"] || join4(getDataDir(), "machines.db");
|
|
4144
4664
|
}
|
|
4145
4665
|
function getManifestPath() {
|
|
4146
|
-
return process.env["HASNA_MACHINES_MANIFEST_PATH"] ||
|
|
4666
|
+
return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join4(getDataDir(), "machines.json");
|
|
4147
4667
|
}
|
|
4148
4668
|
function getNotificationsPath() {
|
|
4149
|
-
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] ||
|
|
4669
|
+
return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join4(getDataDir(), "notifications.json");
|
|
4150
4670
|
}
|
|
4151
4671
|
function ensureParentDir(filePath) {
|
|
4152
4672
|
if (filePath === ":memory:")
|
|
4153
4673
|
return;
|
|
4154
4674
|
const dir = dirname2(resolve(filePath));
|
|
4155
|
-
if (!
|
|
4675
|
+
if (!existsSync3(dir)) {
|
|
4156
4676
|
mkdirSync(dir, { recursive: true });
|
|
4157
4677
|
}
|
|
4158
4678
|
}
|
|
@@ -4194,7 +4714,7 @@ var fleetSchema = exports_external.object({
|
|
|
4194
4714
|
machines: exports_external.array(machineSchema)
|
|
4195
4715
|
});
|
|
4196
4716
|
function detectWorkspacePath() {
|
|
4197
|
-
const home =
|
|
4717
|
+
const home = homedir3();
|
|
4198
4718
|
if (platform() === "darwin") {
|
|
4199
4719
|
return `${home}/Workspace`;
|
|
4200
4720
|
}
|
|
@@ -4218,7 +4738,7 @@ function getDefaultManifest() {
|
|
|
4218
4738
|
};
|
|
4219
4739
|
}
|
|
4220
4740
|
function readManifest(path = getManifestPath()) {
|
|
4221
|
-
if (!
|
|
4741
|
+
if (!existsSync4(path)) {
|
|
4222
4742
|
return getDefaultManifest();
|
|
4223
4743
|
}
|
|
4224
4744
|
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -4354,19 +4874,19 @@ function countRuns(table) {
|
|
|
4354
4874
|
}
|
|
4355
4875
|
function recordSetupRun(machineId, status, details) {
|
|
4356
4876
|
const db = getDb();
|
|
4357
|
-
const
|
|
4877
|
+
const now2 = new Date().toISOString();
|
|
4358
4878
|
db.query(`INSERT INTO setup_runs (id, machine_id, status, details_json, created_at, updated_at)
|
|
4359
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details),
|
|
4879
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(details), now2, now2);
|
|
4360
4880
|
}
|
|
4361
4881
|
function recordSyncRun(machineId, status, actions) {
|
|
4362
4882
|
const db = getDb();
|
|
4363
|
-
const
|
|
4883
|
+
const now2 = new Date().toISOString();
|
|
4364
4884
|
db.query(`INSERT INTO sync_runs (id, machine_id, status, actions_json, created_at, updated_at)
|
|
4365
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions),
|
|
4885
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now2, now2);
|
|
4366
4886
|
}
|
|
4367
4887
|
|
|
4368
4888
|
// src/topology.ts
|
|
4369
|
-
import { existsSync as
|
|
4889
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4370
4890
|
import { arch as arch2, hostname as hostname4, platform as platform2, userInfo as userInfo2 } from "os";
|
|
4371
4891
|
import { spawnSync } from "child_process";
|
|
4372
4892
|
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
@@ -4556,7 +5076,7 @@ function buildEntry(input) {
|
|
|
4556
5076
|
};
|
|
4557
5077
|
}
|
|
4558
5078
|
function discoverMachineTopology(options = {}) {
|
|
4559
|
-
const
|
|
5079
|
+
const now2 = options.now ?? new Date;
|
|
4560
5080
|
const runner = options.runner ?? defaultRunner;
|
|
4561
5081
|
const warnings = [];
|
|
4562
5082
|
const manifest = readManifest();
|
|
@@ -4588,11 +5108,11 @@ function discoverMachineTopology(options = {}) {
|
|
|
4588
5108
|
version: getPackageVersion()
|
|
4589
5109
|
},
|
|
4590
5110
|
capabilities: getMachinesConsumerCapabilities(),
|
|
4591
|
-
generated_at:
|
|
5111
|
+
generated_at: now2.toISOString(),
|
|
4592
5112
|
local_machine_id: localMachineId,
|
|
4593
5113
|
local_hostname: hostname4(),
|
|
4594
5114
|
current_platform: normalizePlatform2(),
|
|
4595
|
-
manifest_path_known:
|
|
5115
|
+
manifest_path_known: existsSync5(getManifestPath()),
|
|
4596
5116
|
machines,
|
|
4597
5117
|
warnings
|
|
4598
5118
|
};
|
|
@@ -4711,11 +5231,11 @@ function cacheability(input) {
|
|
|
4711
5231
|
};
|
|
4712
5232
|
}
|
|
4713
5233
|
function resolveMachineRoute(machineId, options = {}) {
|
|
4714
|
-
const
|
|
5234
|
+
const now2 = options.now ?? new Date;
|
|
4715
5235
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
4716
5236
|
const warnings = [...topology.warnings];
|
|
4717
5237
|
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
4718
|
-
const generatedAt =
|
|
5238
|
+
const generatedAt = now2.toISOString();
|
|
4719
5239
|
if (!machine) {
|
|
4720
5240
|
warnings.push(`machine_not_found:${machineId}`);
|
|
4721
5241
|
return {
|
|
@@ -4741,8 +5261,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
4741
5261
|
},
|
|
4742
5262
|
cacheability: cacheability({
|
|
4743
5263
|
ok: false,
|
|
4744
|
-
observedAt:
|
|
4745
|
-
now,
|
|
5264
|
+
observedAt: now2,
|
|
5265
|
+
now: now2,
|
|
4746
5266
|
ttlMs: options.resolverTtlMs,
|
|
4747
5267
|
authority: "unresolved",
|
|
4748
5268
|
confidence: "none",
|
|
@@ -4779,8 +5299,8 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
4779
5299
|
},
|
|
4780
5300
|
cacheability: cacheability({
|
|
4781
5301
|
ok,
|
|
4782
|
-
observedAt:
|
|
4783
|
-
now,
|
|
5302
|
+
observedAt: now2,
|
|
5303
|
+
now: now2,
|
|
4784
5304
|
ttlMs: options.resolverTtlMs,
|
|
4785
5305
|
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
4786
5306
|
confidence,
|
|
@@ -4867,7 +5387,7 @@ function canCheckPathForMachine(machine, localMachineId) {
|
|
|
4867
5387
|
function checkedPathExists(path, check) {
|
|
4868
5388
|
if (!path || !check)
|
|
4869
5389
|
return null;
|
|
4870
|
-
return
|
|
5390
|
+
return existsSync5(path);
|
|
4871
5391
|
}
|
|
4872
5392
|
function repairHint(input) {
|
|
4873
5393
|
const command = [
|
|
@@ -5082,11 +5602,11 @@ function metadataKeysForDiagnostics(metadata) {
|
|
|
5082
5602
|
return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
|
|
5083
5603
|
}
|
|
5084
5604
|
function resolveMachineWorkspace(options) {
|
|
5085
|
-
const
|
|
5605
|
+
const now2 = options.now ?? new Date;
|
|
5086
5606
|
const topology = options.topology ?? discoverMachineTopology(options);
|
|
5087
5607
|
const warnings = [...topology.warnings];
|
|
5088
5608
|
const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
|
|
5089
|
-
const generatedAt =
|
|
5609
|
+
const generatedAt = now2.toISOString();
|
|
5090
5610
|
const repoName = options.repoName ?? options.projectId;
|
|
5091
5611
|
const openFilesRepoName = options.openFilesRepoName ?? "open-files";
|
|
5092
5612
|
if (!machine) {
|
|
@@ -5115,8 +5635,8 @@ function resolveMachineWorkspace(options) {
|
|
|
5115
5635
|
},
|
|
5116
5636
|
cacheability: cacheability({
|
|
5117
5637
|
ok: false,
|
|
5118
|
-
observedAt:
|
|
5119
|
-
now,
|
|
5638
|
+
observedAt: now2,
|
|
5639
|
+
now: now2,
|
|
5120
5640
|
ttlMs: options.resolverTtlMs,
|
|
5121
5641
|
authority: "unresolved",
|
|
5122
5642
|
confidence: "none",
|
|
@@ -5188,8 +5708,8 @@ function resolveMachineWorkspace(options) {
|
|
|
5188
5708
|
},
|
|
5189
5709
|
cacheability: cacheability({
|
|
5190
5710
|
ok: workspaceOk,
|
|
5191
|
-
observedAt:
|
|
5192
|
-
now,
|
|
5711
|
+
observedAt: now2,
|
|
5712
|
+
now: now2,
|
|
5193
5713
|
ttlMs: options.resolverTtlMs,
|
|
5194
5714
|
authority: workspaceAuthority(workspacePaths),
|
|
5195
5715
|
confidence: workspaceOk ? "medium" : "none",
|
|
@@ -5414,21 +5934,21 @@ function runAppsInstall(machineId, options = {}) {
|
|
|
5414
5934
|
}
|
|
5415
5935
|
|
|
5416
5936
|
// src/commands/cert.ts
|
|
5417
|
-
import { homedir as
|
|
5418
|
-
import { join as
|
|
5937
|
+
import { homedir as homedir4, platform as platform3 } from "os";
|
|
5938
|
+
import { join as join5 } from "path";
|
|
5419
5939
|
function quote2(value) {
|
|
5420
5940
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5421
5941
|
}
|
|
5422
5942
|
function certDir() {
|
|
5423
|
-
return
|
|
5943
|
+
return join5(homedir4(), ".hasna", "machines", "certs");
|
|
5424
5944
|
}
|
|
5425
5945
|
function buildCertPlan(domains) {
|
|
5426
5946
|
if (domains.length === 0) {
|
|
5427
5947
|
throw new Error("At least one domain is required.");
|
|
5428
5948
|
}
|
|
5429
5949
|
const primary = domains[0];
|
|
5430
|
-
const certPath =
|
|
5431
|
-
const keyPath =
|
|
5950
|
+
const certPath = join5(certDir(), `${primary}.pem`);
|
|
5951
|
+
const keyPath = join5(certDir(), `${primary}-key.pem`);
|
|
5432
5952
|
const steps = [];
|
|
5433
5953
|
if (platform3() === "darwin") {
|
|
5434
5954
|
steps.push({
|
|
@@ -5492,14 +6012,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
5492
6012
|
}
|
|
5493
6013
|
|
|
5494
6014
|
// src/commands/dns.ts
|
|
5495
|
-
import { existsSync as
|
|
5496
|
-
import { join as
|
|
6015
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
6016
|
+
import { join as join6 } from "path";
|
|
5497
6017
|
function getDnsPath() {
|
|
5498
|
-
return
|
|
6018
|
+
return join6(getDataDir(), "dns.json");
|
|
5499
6019
|
}
|
|
5500
6020
|
function readMappings() {
|
|
5501
6021
|
const path = getDnsPath();
|
|
5502
|
-
if (!
|
|
6022
|
+
if (!existsSync6(path))
|
|
5503
6023
|
return [];
|
|
5504
6024
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
5505
6025
|
}
|
|
@@ -5528,10 +6048,10 @@ function renderDomainMapping(domain) {
|
|
|
5528
6048
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
5529
6049
|
caddySnippet: `${entry.domain} {
|
|
5530
6050
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
5531
|
-
tls ${
|
|
6051
|
+
tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
5532
6052
|
}`,
|
|
5533
|
-
certPath:
|
|
5534
|
-
keyPath:
|
|
6053
|
+
certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
6054
|
+
keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
5535
6055
|
};
|
|
5536
6056
|
}
|
|
5537
6057
|
|
|
@@ -5803,7 +6323,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
5803
6323
|
}
|
|
5804
6324
|
|
|
5805
6325
|
// src/commands/notifications.ts
|
|
5806
|
-
import { existsSync as
|
|
6326
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
5807
6327
|
var notificationChannelSchema = exports_external.object({
|
|
5808
6328
|
id: exports_external.string(),
|
|
5809
6329
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -5885,7 +6405,7 @@ ${message}
|
|
|
5885
6405
|
}
|
|
5886
6406
|
throw new Error("No local email transport available. Install sendmail or mail.");
|
|
5887
6407
|
}
|
|
5888
|
-
async function
|
|
6408
|
+
async function dispatchWebhook2(channel, event, message) {
|
|
5889
6409
|
const response = await fetch(channel.target, {
|
|
5890
6410
|
method: "POST",
|
|
5891
6411
|
headers: {
|
|
@@ -5910,7 +6430,7 @@ async function dispatchWebhook(channel, event, message) {
|
|
|
5910
6430
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
5911
6431
|
};
|
|
5912
6432
|
}
|
|
5913
|
-
async function
|
|
6433
|
+
async function dispatchCommand2(channel, event, message) {
|
|
5914
6434
|
const result = Bun.spawnSync(["bash", "-lc", channel.target], {
|
|
5915
6435
|
stdout: "pipe",
|
|
5916
6436
|
stderr: "pipe",
|
|
@@ -5933,7 +6453,7 @@ async function dispatchCommand(channel, event, message) {
|
|
|
5933
6453
|
detail: stdout || "Command completed successfully"
|
|
5934
6454
|
};
|
|
5935
6455
|
}
|
|
5936
|
-
async function
|
|
6456
|
+
async function dispatchChannel2(channel, event, message) {
|
|
5937
6457
|
if (!channel.enabled) {
|
|
5938
6458
|
return {
|
|
5939
6459
|
channelId: channel.id,
|
|
@@ -5947,9 +6467,9 @@ async function dispatchChannel(channel, event, message) {
|
|
|
5947
6467
|
return dispatchEmail(channel, event, message);
|
|
5948
6468
|
}
|
|
5949
6469
|
if (channel.type === "webhook") {
|
|
5950
|
-
return
|
|
6470
|
+
return dispatchWebhook2(channel, event, message);
|
|
5951
6471
|
}
|
|
5952
|
-
return
|
|
6472
|
+
return dispatchCommand2(channel, event, message);
|
|
5953
6473
|
}
|
|
5954
6474
|
function getDefaultNotificationConfig() {
|
|
5955
6475
|
return {
|
|
@@ -5959,7 +6479,7 @@ function getDefaultNotificationConfig() {
|
|
|
5959
6479
|
};
|
|
5960
6480
|
}
|
|
5961
6481
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
5962
|
-
if (!
|
|
6482
|
+
if (!existsSync7(path)) {
|
|
5963
6483
|
return getDefaultNotificationConfig();
|
|
5964
6484
|
}
|
|
5965
6485
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -6004,7 +6524,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
6004
6524
|
const deliveries = [];
|
|
6005
6525
|
for (const channel of channels) {
|
|
6006
6526
|
try {
|
|
6007
|
-
deliveries.push(await
|
|
6527
|
+
deliveries.push(await dispatchChannel2(channel, event, message));
|
|
6008
6528
|
} catch (error) {
|
|
6009
6529
|
deliveries.push({
|
|
6010
6530
|
channelId: channel.id,
|
|
@@ -6039,7 +6559,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
6039
6559
|
if (!options.yes) {
|
|
6040
6560
|
throw new Error("Notification test execution requires --yes.");
|
|
6041
6561
|
}
|
|
6042
|
-
const delivery = await
|
|
6562
|
+
const delivery = await dispatchChannel2(channel, event, message);
|
|
6043
6563
|
return {
|
|
6044
6564
|
channelId,
|
|
6045
6565
|
mode: "apply",
|
|
@@ -6214,13 +6734,16 @@ function getServeInfo(options = {}) {
|
|
|
6214
6734
|
"/api/status",
|
|
6215
6735
|
"/api/manifest",
|
|
6216
6736
|
"/api/notifications",
|
|
6737
|
+
"/api/webhooks",
|
|
6738
|
+
"/api/events",
|
|
6217
6739
|
"/api/doctor",
|
|
6218
6740
|
"/api/self-test",
|
|
6219
6741
|
"/api/apps/status",
|
|
6220
6742
|
"/api/apps/diff",
|
|
6221
6743
|
"/api/install-claude/status",
|
|
6222
6744
|
"/api/install-claude/diff",
|
|
6223
|
-
"/api/notifications/test"
|
|
6745
|
+
"/api/notifications/test",
|
|
6746
|
+
"/api/webhooks/test"
|
|
6224
6747
|
]
|
|
6225
6748
|
};
|
|
6226
6749
|
}
|
|
@@ -6377,7 +6900,7 @@ function renderDashboardHtml() {
|
|
|
6377
6900
|
}
|
|
6378
6901
|
|
|
6379
6902
|
// src/commands/setup.ts
|
|
6380
|
-
import { homedir as
|
|
6903
|
+
import { homedir as homedir5 } from "os";
|
|
6381
6904
|
function quote3(value) {
|
|
6382
6905
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6383
6906
|
}
|
|
@@ -6447,7 +6970,7 @@ function buildSetupPlan(machineId) {
|
|
|
6447
6970
|
const target = selected || {
|
|
6448
6971
|
id: currentMachineId,
|
|
6449
6972
|
platform: "linux",
|
|
6450
|
-
workspacePath: `${
|
|
6973
|
+
workspacePath: `${homedir5()}/workspace`
|
|
6451
6974
|
};
|
|
6452
6975
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
6453
6976
|
return {
|
|
@@ -6493,8 +7016,8 @@ function runSetup(machineId, options = {}) {
|
|
|
6493
7016
|
}
|
|
6494
7017
|
|
|
6495
7018
|
// src/commands/sync.ts
|
|
6496
|
-
import { existsSync as
|
|
6497
|
-
import { homedir as
|
|
7019
|
+
import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
7020
|
+
import { homedir as homedir6 } from "os";
|
|
6498
7021
|
function quote4(value) {
|
|
6499
7022
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6500
7023
|
}
|
|
@@ -6542,8 +7065,8 @@ function detectPackageActions(machine) {
|
|
|
6542
7065
|
}
|
|
6543
7066
|
function detectFileActions(machine) {
|
|
6544
7067
|
return (machine.files || []).map((file, index) => {
|
|
6545
|
-
const sourceExists =
|
|
6546
|
-
const targetExists =
|
|
7068
|
+
const sourceExists = existsSync8(file.source);
|
|
7069
|
+
const targetExists = existsSync8(file.target);
|
|
6547
7070
|
let status = "missing";
|
|
6548
7071
|
if (sourceExists && targetExists) {
|
|
6549
7072
|
if (file.mode === "symlink") {
|
|
@@ -6571,7 +7094,7 @@ function buildSyncPlan(machineId) {
|
|
|
6571
7094
|
const target = selected || {
|
|
6572
7095
|
id: currentMachineId,
|
|
6573
7096
|
platform: "linux",
|
|
6574
|
-
workspacePath: `${
|
|
7097
|
+
workspacePath: `${homedir6()}/workspace`
|
|
6575
7098
|
};
|
|
6576
7099
|
const actions = [
|
|
6577
7100
|
...detectPackageActions(target),
|
|
@@ -7163,7 +7686,7 @@ function upsertSqlite(db, table, columns, rows) {
|
|
|
7163
7686
|
}
|
|
7164
7687
|
function recordSyncMeta(db, direction, results) {
|
|
7165
7688
|
ensureSyncMetaTable(db);
|
|
7166
|
-
const
|
|
7689
|
+
const now2 = new Date().toISOString();
|
|
7167
7690
|
const statement = db.query(`
|
|
7168
7691
|
INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
|
|
7169
7692
|
VALUES (?, ?, ?)
|
|
@@ -7172,7 +7695,7 @@ function recordSyncMeta(db, direction, results) {
|
|
|
7172
7695
|
for (const result of results) {
|
|
7173
7696
|
if (result.errors.length > 0)
|
|
7174
7697
|
continue;
|
|
7175
|
-
statement.run(result.table,
|
|
7698
|
+
statement.run(result.table, now2, direction);
|
|
7176
7699
|
}
|
|
7177
7700
|
}
|
|
7178
7701
|
function ensureSyncMetaTable(db) {
|
|
@@ -7222,6 +7745,7 @@ function buildServer(version = getPackageVersion()) {
|
|
|
7222
7745
|
}
|
|
7223
7746
|
function createMcpServer(version) {
|
|
7224
7747
|
const server = new McpServer({ name: "machines", version });
|
|
7748
|
+
const events = new EventsClient;
|
|
7225
7749
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
7226
7750
|
content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
|
|
7227
7751
|
}));
|
|
@@ -7364,8 +7888,8 @@ function createMcpServer(version) {
|
|
|
7364
7888
|
target: exports_external.string().describe("Email, webhook URL, or shell command"),
|
|
7365
7889
|
events: exports_external.array(exports_external.string()).describe("Events routed to this channel"),
|
|
7366
7890
|
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
7367
|
-
}, async ({ channel_id, type, target, events, enabled }) => ({
|
|
7368
|
-
content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events, enabled: enabled ?? true }), null, 2) }]
|
|
7891
|
+
}, async ({ channel_id, type, target, events: events2, enabled }) => ({
|
|
7892
|
+
content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events: events2, enabled: enabled ?? true }), null, 2) }]
|
|
7369
7893
|
}));
|
|
7370
7894
|
server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
|
|
7371
7895
|
content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
|
|
@@ -7375,6 +7899,60 @@ function createMcpServer(version) {
|
|
|
7375
7899
|
}));
|
|
7376
7900
|
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) }] }));
|
|
7377
7901
|
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) }] }));
|
|
7902
|
+
server.tool("machines_webhooks_add", "Add or replace a shared Hasna event webhook channel.", {
|
|
7903
|
+
channel_id: exports_external.string().describe("Channel identifier"),
|
|
7904
|
+
url: exports_external.string().url().describe("Webhook URL"),
|
|
7905
|
+
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
7906
|
+
source: exports_external.string().optional().describe("Optional source filter"),
|
|
7907
|
+
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
7908
|
+
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
7909
|
+
}, async ({ channel_id, url, event_type, source, secret, enabled }) => {
|
|
7910
|
+
const now2 = new Date().toISOString();
|
|
7911
|
+
const channel = await events.addChannel({
|
|
7912
|
+
id: channel_id,
|
|
7913
|
+
enabled: enabled ?? true,
|
|
7914
|
+
transport: "webhook",
|
|
7915
|
+
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
7916
|
+
webhook: { url, secret },
|
|
7917
|
+
createdAt: now2,
|
|
7918
|
+
updatedAt: now2
|
|
7919
|
+
});
|
|
7920
|
+
return { content: [{ type: "text", text: JSON.stringify(sanitizeChannelForOutput(channel), null, 2) }] };
|
|
7921
|
+
});
|
|
7922
|
+
server.tool("machines_webhooks_list", "List shared Hasna event webhook channels.", {}, async () => ({
|
|
7923
|
+
content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput(await events.listChannels()), null, 2) }]
|
|
7924
|
+
}));
|
|
7925
|
+
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 }) => ({
|
|
7926
|
+
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
7927
|
+
}));
|
|
7928
|
+
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) }] }));
|
|
7929
|
+
server.tool("machines_events_emit", "Emit a shared Hasna event from machines.", {
|
|
7930
|
+
event_type: exports_external.string().describe("Event type"),
|
|
7931
|
+
subject: exports_external.string().optional().describe("Event subject"),
|
|
7932
|
+
severity: exports_external.enum(["debug", "info", "notice", "warning", "error", "critical"]).optional().describe("Event severity"),
|
|
7933
|
+
message: exports_external.string().optional().describe("Message body"),
|
|
7934
|
+
data: exports_external.record(exports_external.unknown()).optional().describe("Event data"),
|
|
7935
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Event metadata"),
|
|
7936
|
+
dedupe_key: exports_external.string().optional().describe("Dedupe key"),
|
|
7937
|
+
deliver: exports_external.boolean().optional().describe("Deliver to matching channels")
|
|
7938
|
+
}, async ({ event_type, subject, severity, message, data, metadata, dedupe_key, deliver }) => ({
|
|
7939
|
+
content: [{ type: "text", text: JSON.stringify(await events.emit({
|
|
7940
|
+
source: "machines",
|
|
7941
|
+
type: event_type,
|
|
7942
|
+
subject,
|
|
7943
|
+
severity,
|
|
7944
|
+
message,
|
|
7945
|
+
data: data ?? {},
|
|
7946
|
+
metadata: metadata ?? {},
|
|
7947
|
+
dedupeKey: dedupe_key
|
|
7948
|
+
}, { deliver: deliver !== false }), null, 2) }]
|
|
7949
|
+
}));
|
|
7950
|
+
server.tool("machines_events_list", "List shared Hasna events.", {}, async () => ({
|
|
7951
|
+
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
7952
|
+
}));
|
|
7953
|
+
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 }) => ({
|
|
7954
|
+
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
7955
|
+
}));
|
|
7378
7956
|
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) }] }));
|
|
7379
7957
|
server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
|
|
7380
7958
|
content: [{ type: "text", text: renderDashboardHtml() }]
|