@hasna/machines 0.0.16 → 0.0.18
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 +17 -5
- package/dist/cli/index.js +400 -227
- package/dist/commands/ssh.d.ts +6 -3
- package/dist/commands/ssh.d.ts.map +1 -1
- package/dist/compatibility.d.ts +4 -0
- package/dist/compatibility.d.ts.map +1 -1
- package/dist/consumer.d.ts +10 -0
- package/dist/consumer.d.ts.map +1 -0
- package/dist/consumer.js +4986 -0
- package/dist/index.js +235 -73
- package/dist/mcp/index.js +383 -227
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/topology.d.ts +45 -1
- package/dist/topology.d.ts.map +1 -1
- package/package.json +6 -2
package/dist/mcp/index.js
CHANGED
|
@@ -4218,7 +4218,7 @@ function detectCurrentMachineManifest() {
|
|
|
4218
4218
|
|
|
4219
4219
|
// src/remote.ts
|
|
4220
4220
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
4221
|
-
import { hostname as
|
|
4221
|
+
import { hostname as hostname4 } from "os";
|
|
4222
4222
|
|
|
4223
4223
|
// src/db.ts
|
|
4224
4224
|
import { Database } from "bun:sqlite";
|
|
@@ -4319,55 +4319,373 @@ function recordSyncRun(machineId, status, actions) {
|
|
|
4319
4319
|
VALUES (?, ?, ?, ?, ?, ?)`).run(crypto.randomUUID(), machineId, status, JSON.stringify(actions), now, now);
|
|
4320
4320
|
}
|
|
4321
4321
|
|
|
4322
|
-
// src/
|
|
4322
|
+
// src/topology.ts
|
|
4323
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4324
|
+
import { arch as arch2, hostname as hostname3, platform as platform2, userInfo as userInfo2 } from "os";
|
|
4323
4325
|
import { spawnSync } from "child_process";
|
|
4326
|
+
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
4327
|
+
var MACHINES_PACKAGE_NAME = "@hasna/machines";
|
|
4328
|
+
function normalizePlatform2(value = platform2()) {
|
|
4329
|
+
const normalized = value.toLowerCase();
|
|
4330
|
+
if (normalized === "darwin" || normalized === "macos")
|
|
4331
|
+
return "macos";
|
|
4332
|
+
if (normalized === "win32" || normalized === "windows")
|
|
4333
|
+
return "windows";
|
|
4334
|
+
if (normalized === "linux")
|
|
4335
|
+
return "linux";
|
|
4336
|
+
return value;
|
|
4337
|
+
}
|
|
4338
|
+
function defaultRunner(command) {
|
|
4339
|
+
const result = spawnSync("bash", ["-c", command], {
|
|
4340
|
+
encoding: "utf8",
|
|
4341
|
+
env: process.env
|
|
4342
|
+
});
|
|
4343
|
+
return {
|
|
4344
|
+
stdout: result.stdout || "",
|
|
4345
|
+
stderr: result.stderr || "",
|
|
4346
|
+
exitCode: result.status ?? 1
|
|
4347
|
+
};
|
|
4348
|
+
}
|
|
4349
|
+
function hasCommand(command, runner) {
|
|
4350
|
+
return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
|
|
4351
|
+
}
|
|
4352
|
+
function parseTailscaleStatus(raw) {
|
|
4353
|
+
try {
|
|
4354
|
+
const parsed = JSON.parse(raw);
|
|
4355
|
+
if (!parsed || typeof parsed !== "object")
|
|
4356
|
+
return null;
|
|
4357
|
+
return parsed;
|
|
4358
|
+
} catch {
|
|
4359
|
+
return null;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
function loadTailscalePeers(runner, warnings) {
|
|
4363
|
+
const peers = new Map;
|
|
4364
|
+
if (!hasCommand("tailscale", runner)) {
|
|
4365
|
+
warnings.push("tailscale_not_available");
|
|
4366
|
+
return peers;
|
|
4367
|
+
}
|
|
4368
|
+
const result = runner("tailscale status --json");
|
|
4369
|
+
if (result.exitCode !== 0) {
|
|
4370
|
+
warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
|
|
4371
|
+
return peers;
|
|
4372
|
+
}
|
|
4373
|
+
const status = parseTailscaleStatus(result.stdout);
|
|
4374
|
+
if (!status) {
|
|
4375
|
+
warnings.push("tailscale_status_invalid_json");
|
|
4376
|
+
return peers;
|
|
4377
|
+
}
|
|
4378
|
+
const addPeer = (peer) => {
|
|
4379
|
+
if (!peer)
|
|
4380
|
+
return;
|
|
4381
|
+
const id = peer.HostName || peer.DNSName?.split(".")[0];
|
|
4382
|
+
if (id)
|
|
4383
|
+
peers.set(id, peer);
|
|
4384
|
+
};
|
|
4385
|
+
addPeer(status.Self);
|
|
4386
|
+
for (const peer of Object.values(status.Peer ?? {}))
|
|
4387
|
+
addPeer(peer);
|
|
4388
|
+
return peers;
|
|
4389
|
+
}
|
|
4390
|
+
function machineKeys(machine) {
|
|
4391
|
+
return [
|
|
4392
|
+
machine.id,
|
|
4393
|
+
machine.hostname,
|
|
4394
|
+
machine.tailscaleName?.split(".")[0],
|
|
4395
|
+
machine.tailscaleName,
|
|
4396
|
+
machine.sshAddress?.split("@").pop()
|
|
4397
|
+
].filter((value) => Boolean(value));
|
|
4398
|
+
}
|
|
4399
|
+
function findTailscalePeer(machine, machineId, peers) {
|
|
4400
|
+
if (machine) {
|
|
4401
|
+
for (const key of machineKeys(machine)) {
|
|
4402
|
+
const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
|
|
4403
|
+
if (peer)
|
|
4404
|
+
return peer;
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
return peers.get(machineId) ?? null;
|
|
4408
|
+
}
|
|
4324
4409
|
function envReachableHosts() {
|
|
4325
4410
|
const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
|
|
4326
4411
|
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
4327
4412
|
}
|
|
4328
|
-
function
|
|
4413
|
+
function manifestHostReachable(target) {
|
|
4329
4414
|
const overrides = envReachableHosts();
|
|
4330
|
-
if (overrides.size
|
|
4331
|
-
return
|
|
4415
|
+
if (overrides.size === 0)
|
|
4416
|
+
return null;
|
|
4417
|
+
return overrides.has(target);
|
|
4418
|
+
}
|
|
4419
|
+
function routeHints(input) {
|
|
4420
|
+
const hints = [];
|
|
4421
|
+
if (input.machineId === input.localMachineId) {
|
|
4422
|
+
hints.push({ kind: "local", target: "localhost", reachable: true });
|
|
4423
|
+
}
|
|
4424
|
+
if (input.manifest?.sshAddress) {
|
|
4425
|
+
hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: manifestHostReachable(input.manifest.sshAddress) });
|
|
4332
4426
|
}
|
|
4333
|
-
|
|
4334
|
-
|
|
4427
|
+
if (input.manifest?.hostname) {
|
|
4428
|
+
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
|
|
4429
|
+
}
|
|
4430
|
+
const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
|
|
4431
|
+
if (tailscaleTarget) {
|
|
4432
|
+
hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
|
|
4433
|
+
}
|
|
4434
|
+
return hints;
|
|
4435
|
+
}
|
|
4436
|
+
function routeRank(hint) {
|
|
4437
|
+
if (hint.kind === "local")
|
|
4438
|
+
return 0;
|
|
4439
|
+
if (hint.reachable === true && hint.kind === "ssh")
|
|
4440
|
+
return 1;
|
|
4441
|
+
if (hint.reachable === true && hint.kind === "lan")
|
|
4442
|
+
return 2;
|
|
4443
|
+
if (hint.reachable === true && hint.kind === "tailscale")
|
|
4444
|
+
return 3;
|
|
4445
|
+
if (hint.reachable === false)
|
|
4446
|
+
return 8;
|
|
4447
|
+
if (hint.kind === "ssh")
|
|
4448
|
+
return 4;
|
|
4449
|
+
if (hint.kind === "lan")
|
|
4450
|
+
return 5;
|
|
4451
|
+
if (hint.kind === "tailscale")
|
|
4452
|
+
return 6;
|
|
4453
|
+
return 9;
|
|
4454
|
+
}
|
|
4455
|
+
function selectRouteHint(hints) {
|
|
4456
|
+
return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
|
|
4457
|
+
}
|
|
4458
|
+
function buildEntry(input) {
|
|
4459
|
+
const manifest = input.manifest;
|
|
4460
|
+
const peer = input.peer;
|
|
4461
|
+
const hints = routeHints({
|
|
4462
|
+
machineId: input.machineId,
|
|
4463
|
+
localMachineId: input.localMachineId,
|
|
4464
|
+
manifest,
|
|
4465
|
+
peer
|
|
4335
4466
|
});
|
|
4336
|
-
|
|
4467
|
+
const selectedRoute = selectRouteHint(hints);
|
|
4468
|
+
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
4469
|
+
return {
|
|
4470
|
+
machine_id: input.machineId,
|
|
4471
|
+
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
4472
|
+
platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
|
|
4473
|
+
os: peer?.OS ?? null,
|
|
4474
|
+
user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
|
|
4475
|
+
workspace_path: manifest?.workspacePath ?? null,
|
|
4476
|
+
manifest_declared: Boolean(manifest),
|
|
4477
|
+
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
4478
|
+
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
4479
|
+
tailscale: {
|
|
4480
|
+
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
4481
|
+
ips: peer?.TailscaleIPs ?? [],
|
|
4482
|
+
online: peer?.Online ?? null,
|
|
4483
|
+
active: peer?.Active ?? null,
|
|
4484
|
+
last_seen: peer?.LastSeen ?? null
|
|
4485
|
+
},
|
|
4486
|
+
ssh: {
|
|
4487
|
+
address: manifest?.sshAddress ?? null,
|
|
4488
|
+
route,
|
|
4489
|
+
command_target: selectedRoute?.target ?? null
|
|
4490
|
+
},
|
|
4491
|
+
route_hints: hints,
|
|
4492
|
+
tags: manifest?.tags ?? [],
|
|
4493
|
+
metadata: manifest?.metadata ?? {}
|
|
4494
|
+
};
|
|
4337
4495
|
}
|
|
4338
|
-
function
|
|
4339
|
-
const
|
|
4340
|
-
|
|
4341
|
-
|
|
4496
|
+
function discoverMachineTopology(options = {}) {
|
|
4497
|
+
const now = options.now ?? new Date;
|
|
4498
|
+
const runner = options.runner ?? defaultRunner;
|
|
4499
|
+
const warnings = [];
|
|
4500
|
+
const manifest = readManifest();
|
|
4501
|
+
const heartbeats = listHeartbeats();
|
|
4502
|
+
const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
|
|
4503
|
+
const localMachineId = getLocalMachineId();
|
|
4504
|
+
const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
|
|
4505
|
+
const machineIds = new Set([
|
|
4506
|
+
localMachineId,
|
|
4507
|
+
...manifest.machines.map((machine) => machine.id),
|
|
4508
|
+
...heartbeats.map((heartbeat) => heartbeat.machine_id),
|
|
4509
|
+
...peers.keys()
|
|
4510
|
+
]);
|
|
4511
|
+
const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
|
|
4512
|
+
const machines = [...machineIds].sort().map((machineId) => {
|
|
4513
|
+
const manifestMachine = manifestById.get(machineId);
|
|
4514
|
+
return buildEntry({
|
|
4515
|
+
machineId,
|
|
4516
|
+
localMachineId,
|
|
4517
|
+
manifest: manifestMachine,
|
|
4518
|
+
peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
|
|
4519
|
+
heartbeat: heartbeatByMachine.get(machineId)
|
|
4520
|
+
});
|
|
4521
|
+
});
|
|
4522
|
+
return {
|
|
4523
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
4524
|
+
package: {
|
|
4525
|
+
name: MACHINES_PACKAGE_NAME,
|
|
4526
|
+
version: getPackageVersion()
|
|
4527
|
+
},
|
|
4528
|
+
capabilities: {
|
|
4529
|
+
topology: true,
|
|
4530
|
+
compatibility: true,
|
|
4531
|
+
route_resolution: true,
|
|
4532
|
+
cli_json_fallback: true
|
|
4533
|
+
},
|
|
4534
|
+
generated_at: now.toISOString(),
|
|
4535
|
+
local_machine_id: localMachineId,
|
|
4536
|
+
local_hostname: hostname3(),
|
|
4537
|
+
current_platform: normalizePlatform2(),
|
|
4538
|
+
manifest_path_known: existsSync4(getManifestPath()),
|
|
4539
|
+
machines,
|
|
4540
|
+
warnings
|
|
4541
|
+
};
|
|
4542
|
+
}
|
|
4543
|
+
function normalizeMachineAlias(value) {
|
|
4544
|
+
return value.trim().replace(/\.$/, "").toLowerCase();
|
|
4545
|
+
}
|
|
4546
|
+
function routeTargetMatches(machine, requested) {
|
|
4547
|
+
const normalized = normalizeMachineAlias(requested);
|
|
4548
|
+
const values = [
|
|
4549
|
+
machine.ssh.address,
|
|
4550
|
+
machine.ssh.command_target,
|
|
4551
|
+
machine.tailscale.dns_name,
|
|
4552
|
+
machine.tailscale.dns_name?.split(".")[0],
|
|
4553
|
+
...machine.tailscale.ips,
|
|
4554
|
+
...machine.route_hints.map((hint) => hint.target),
|
|
4555
|
+
...machine.route_hints.map((hint) => hint.target.split("@").pop() ?? hint.target)
|
|
4556
|
+
].filter((value) => Boolean(value));
|
|
4557
|
+
return values.some((value) => normalizeMachineAlias(value) === normalized);
|
|
4558
|
+
}
|
|
4559
|
+
function findRouteMachine(topology, requestedMachineId) {
|
|
4560
|
+
const requested = normalizeMachineAlias(requestedMachineId);
|
|
4561
|
+
if (requested === "local" || requested === "localhost" || requested === normalizeMachineAlias(hostname3()) || requested === normalizeMachineAlias(topology.local_machine_id)) {
|
|
4562
|
+
return {
|
|
4563
|
+
machine: topology.machines.find((machine) => machine.machine_id === topology.local_machine_id) ?? null,
|
|
4564
|
+
matchedBy: "local_alias"
|
|
4565
|
+
};
|
|
4342
4566
|
}
|
|
4343
|
-
const
|
|
4344
|
-
if (
|
|
4567
|
+
const machineIdMatch = topology.machines.find((machine) => normalizeMachineAlias(machine.machine_id) === requested);
|
|
4568
|
+
if (machineIdMatch)
|
|
4569
|
+
return { machine: machineIdMatch, matchedBy: "machine_id" };
|
|
4570
|
+
const hostnameMatch = topology.machines.find((machine) => machine.hostname && normalizeMachineAlias(machine.hostname) === requested);
|
|
4571
|
+
if (hostnameMatch)
|
|
4572
|
+
return { machine: hostnameMatch, matchedBy: "hostname" };
|
|
4573
|
+
const tailscaleMatch = topology.machines.find((machine) => {
|
|
4574
|
+
if (!machine.tailscale.dns_name)
|
|
4575
|
+
return false;
|
|
4576
|
+
const dns = normalizeMachineAlias(machine.tailscale.dns_name);
|
|
4577
|
+
return dns === requested || dns.split(".")[0] === requested;
|
|
4578
|
+
});
|
|
4579
|
+
if (tailscaleMatch)
|
|
4580
|
+
return { machine: tailscaleMatch, matchedBy: "tailscale" };
|
|
4581
|
+
const routeMatch = topology.machines.find((machine) => routeTargetMatches(machine, requestedMachineId));
|
|
4582
|
+
if (routeMatch)
|
|
4583
|
+
return { machine: routeMatch, matchedBy: "route_target" };
|
|
4584
|
+
return { machine: null, matchedBy: null };
|
|
4585
|
+
}
|
|
4586
|
+
function routeConfidence(input) {
|
|
4587
|
+
if (input.matchedBy === "local_alias")
|
|
4588
|
+
return "exact";
|
|
4589
|
+
if (input.hint?.kind === "local")
|
|
4590
|
+
return "exact";
|
|
4591
|
+
if (input.hint?.reachable === true)
|
|
4592
|
+
return "high";
|
|
4593
|
+
if (input.machine.manifest_declared && (input.hint?.kind === "ssh" || input.hint?.kind === "lan"))
|
|
4594
|
+
return "medium";
|
|
4595
|
+
if (input.hint)
|
|
4596
|
+
return "low";
|
|
4597
|
+
return "none";
|
|
4598
|
+
}
|
|
4599
|
+
function resolveMachineRoute(machineId, options = {}) {
|
|
4600
|
+
const topology = options.topology ?? discoverMachineTopology(options);
|
|
4601
|
+
const warnings = [...topology.warnings];
|
|
4602
|
+
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
4603
|
+
const generatedAt = (options.now ?? new Date).toISOString();
|
|
4604
|
+
if (!machine) {
|
|
4605
|
+
warnings.push(`machine_not_found:${machineId}`);
|
|
4345
4606
|
return {
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4607
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
4608
|
+
package: { name: MACHINES_PACKAGE_NAME, version: getPackageVersion() },
|
|
4609
|
+
ok: false,
|
|
4610
|
+
machine_id: null,
|
|
4611
|
+
requested_machine_id: machineId,
|
|
4612
|
+
generated_at: generatedAt,
|
|
4613
|
+
route: "unknown",
|
|
4614
|
+
source: "unknown",
|
|
4615
|
+
target: null,
|
|
4616
|
+
command_target: null,
|
|
4617
|
+
confidence: "none",
|
|
4618
|
+
local: false,
|
|
4619
|
+
evidence: {
|
|
4620
|
+
topology: true,
|
|
4621
|
+
matched_by: null,
|
|
4622
|
+
manifest_declared: null,
|
|
4623
|
+
heartbeat_status: null,
|
|
4624
|
+
tailscale_online: null,
|
|
4625
|
+
selected_hint: null
|
|
4626
|
+
},
|
|
4627
|
+
warnings
|
|
4349
4628
|
};
|
|
4350
4629
|
}
|
|
4351
|
-
const
|
|
4352
|
-
const
|
|
4353
|
-
const
|
|
4630
|
+
const selectedHint = selectRouteHint(machine.route_hints);
|
|
4631
|
+
const route = selectedHint?.kind ?? machine.ssh.route ?? "unknown";
|
|
4632
|
+
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
4354
4633
|
return {
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4634
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
4635
|
+
package: topology.package,
|
|
4636
|
+
ok: Boolean(selectedHint?.target),
|
|
4637
|
+
machine_id: machine.machine_id,
|
|
4638
|
+
requested_machine_id: machineId,
|
|
4639
|
+
generated_at: generatedAt,
|
|
4640
|
+
route,
|
|
4641
|
+
source: route,
|
|
4642
|
+
target: selectedHint?.target ?? null,
|
|
4643
|
+
command_target: selectedHint?.target ?? null,
|
|
4644
|
+
confidence: routeConfidence({ machine, hint: selectedHint, matchedBy }),
|
|
4645
|
+
local,
|
|
4646
|
+
evidence: {
|
|
4647
|
+
topology: true,
|
|
4648
|
+
matched_by: matchedBy,
|
|
4649
|
+
manifest_declared: machine.manifest_declared,
|
|
4650
|
+
heartbeat_status: machine.heartbeat_status,
|
|
4651
|
+
tailscale_online: machine.tailscale.online,
|
|
4652
|
+
selected_hint: selectedHint
|
|
4653
|
+
},
|
|
4654
|
+
warnings
|
|
4358
4655
|
};
|
|
4359
4656
|
}
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4657
|
+
|
|
4658
|
+
// src/commands/ssh.ts
|
|
4659
|
+
function shellQuote(value) {
|
|
4660
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
4661
|
+
}
|
|
4662
|
+
function resolveSshTarget(machineId, options = {}) {
|
|
4663
|
+
const resolved = resolveMachineRoute(machineId, options);
|
|
4664
|
+
if (!resolved.ok || !resolved.target) {
|
|
4665
|
+
throw new Error(`Machine route not found: ${machineId}`);
|
|
4666
|
+
}
|
|
4667
|
+
if (resolved.route !== "local" && resolved.route !== "lan" && resolved.route !== "tailscale" && resolved.route !== "ssh") {
|
|
4668
|
+
throw new Error(`Machine route is not SSH-capable: ${machineId}`);
|
|
4669
|
+
}
|
|
4670
|
+
return {
|
|
4671
|
+
machineId: resolved.machine_id ?? machineId,
|
|
4672
|
+
target: resolved.target,
|
|
4673
|
+
route: resolved.route,
|
|
4674
|
+
confidence: resolved.confidence,
|
|
4675
|
+
warnings: resolved.warnings
|
|
4676
|
+
};
|
|
4677
|
+
}
|
|
4678
|
+
function buildSshCommand(machineId, remoteCommand, options = {}) {
|
|
4679
|
+
const resolved = resolveSshTarget(machineId, options);
|
|
4680
|
+
return remoteCommand ? `ssh ${resolved.target} ${shellQuote(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
4363
4681
|
}
|
|
4364
4682
|
|
|
4365
4683
|
// src/remote.ts
|
|
4366
|
-
function
|
|
4684
|
+
function shellQuote2(value) {
|
|
4367
4685
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
4368
4686
|
}
|
|
4369
4687
|
function machineIsLocal(machineId, localMachineId) {
|
|
4370
|
-
return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId ===
|
|
4688
|
+
return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname4();
|
|
4371
4689
|
}
|
|
4372
4690
|
function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
|
|
4373
4691
|
if (machineIsLocal(machineId, localMachineId)) {
|
|
@@ -4379,8 +4697,9 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
|
|
|
4379
4697
|
shellCommand: buildSshCommand(machineId, command)
|
|
4380
4698
|
};
|
|
4381
4699
|
} catch (error) {
|
|
4382
|
-
|
|
4383
|
-
|
|
4700
|
+
const message = String(error.message ?? error);
|
|
4701
|
+
if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
|
|
4702
|
+
return { source: "ssh", shellCommand: `ssh ${shellQuote2(machineId)} ${shellQuote2(command)}` };
|
|
4384
4703
|
}
|
|
4385
4704
|
throw error;
|
|
4386
4705
|
}
|
|
@@ -4413,7 +4732,7 @@ function getAppManager(machine, app) {
|
|
|
4413
4732
|
return "winget";
|
|
4414
4733
|
return "apt";
|
|
4415
4734
|
}
|
|
4416
|
-
function
|
|
4735
|
+
function shellQuote3(value) {
|
|
4417
4736
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4418
4737
|
}
|
|
4419
4738
|
function buildAppCommand(machine, app) {
|
|
@@ -4434,7 +4753,7 @@ function buildAppCommand(machine, app) {
|
|
|
4434
4753
|
return `sudo apt-get install -y ${packageName}`;
|
|
4435
4754
|
}
|
|
4436
4755
|
function buildAppProbeCommand(machine, app) {
|
|
4437
|
-
const packageName =
|
|
4756
|
+
const packageName = shellQuote3(getPackageName(app));
|
|
4438
4757
|
const manager = getAppManager(machine, app);
|
|
4439
4758
|
if (manager === "custom") {
|
|
4440
4759
|
return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
|
|
@@ -4539,7 +4858,7 @@ function runAppsInstall(machineId, options = {}) {
|
|
|
4539
4858
|
}
|
|
4540
4859
|
|
|
4541
4860
|
// src/commands/cert.ts
|
|
4542
|
-
import { homedir as homedir3, platform as
|
|
4861
|
+
import { homedir as homedir3, platform as platform3 } from "os";
|
|
4543
4862
|
import { join as join4 } from "path";
|
|
4544
4863
|
function quote2(value) {
|
|
4545
4864
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -4555,7 +4874,7 @@ function buildCertPlan(domains) {
|
|
|
4555
4874
|
const certPath = join4(certDir(), `${primary}.pem`);
|
|
4556
4875
|
const keyPath = join4(certDir(), `${primary}-key.pem`);
|
|
4557
4876
|
const steps = [];
|
|
4558
|
-
if (
|
|
4877
|
+
if (platform3() === "darwin") {
|
|
4559
4878
|
steps.push({
|
|
4560
4879
|
id: "mkcert-install-macos",
|
|
4561
4880
|
title: "Install mkcert on macOS",
|
|
@@ -4617,14 +4936,14 @@ function runCertPlan(domains, options = {}) {
|
|
|
4617
4936
|
}
|
|
4618
4937
|
|
|
4619
4938
|
// src/commands/dns.ts
|
|
4620
|
-
import { existsSync as
|
|
4939
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
4621
4940
|
import { join as join5 } from "path";
|
|
4622
4941
|
function getDnsPath() {
|
|
4623
4942
|
return join5(getDataDir(), "dns.json");
|
|
4624
4943
|
}
|
|
4625
4944
|
function readMappings() {
|
|
4626
4945
|
const path = getDnsPath();
|
|
4627
|
-
if (!
|
|
4946
|
+
if (!existsSync5(path))
|
|
4628
4947
|
return [];
|
|
4629
4948
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
4630
4949
|
}
|
|
@@ -4928,7 +5247,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
4928
5247
|
}
|
|
4929
5248
|
|
|
4930
5249
|
// src/commands/notifications.ts
|
|
4931
|
-
import { existsSync as
|
|
5250
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
4932
5251
|
var notificationChannelSchema = exports_external.object({
|
|
4933
5252
|
id: exports_external.string(),
|
|
4934
5253
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
@@ -4944,10 +5263,10 @@ var notificationConfigSchema = exports_external.object({
|
|
|
4944
5263
|
function sortChannels(channels) {
|
|
4945
5264
|
return [...channels].sort((left, right) => left.id.localeCompare(right.id));
|
|
4946
5265
|
}
|
|
4947
|
-
function
|
|
5266
|
+
function shellQuote4(value) {
|
|
4948
5267
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4949
5268
|
}
|
|
4950
|
-
function
|
|
5269
|
+
function hasCommand2(binary) {
|
|
4951
5270
|
const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
|
|
4952
5271
|
stdout: "ignore",
|
|
4953
5272
|
stderr: "ignore",
|
|
@@ -4972,7 +5291,7 @@ Content-Type: text/plain; charset=utf-8
|
|
|
4972
5291
|
|
|
4973
5292
|
${message}
|
|
4974
5293
|
`;
|
|
4975
|
-
if (
|
|
5294
|
+
if (hasCommand2("sendmail")) {
|
|
4976
5295
|
const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
|
|
4977
5296
|
stdin: new TextEncoder().encode(body),
|
|
4978
5297
|
stdout: "pipe",
|
|
@@ -4990,8 +5309,8 @@ ${message}
|
|
|
4990
5309
|
detail: `Delivered via sendmail to ${channel.target}`
|
|
4991
5310
|
};
|
|
4992
5311
|
}
|
|
4993
|
-
if (
|
|
4994
|
-
const command = `printf %s ${
|
|
5312
|
+
if (hasCommand2("mail")) {
|
|
5313
|
+
const command = `printf %s ${shellQuote4(message)} | mail -s ${shellQuote4(subject)} ${shellQuote4(channel.target)}`;
|
|
4995
5314
|
const result = Bun.spawnSync(["bash", "-lc", command], {
|
|
4996
5315
|
stdout: "pipe",
|
|
4997
5316
|
stderr: "pipe",
|
|
@@ -5084,7 +5403,7 @@ function getDefaultNotificationConfig() {
|
|
|
5084
5403
|
};
|
|
5085
5404
|
}
|
|
5086
5405
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
5087
|
-
if (!
|
|
5406
|
+
if (!existsSync6(path)) {
|
|
5088
5407
|
return getDefaultNotificationConfig();
|
|
5089
5408
|
}
|
|
5090
5409
|
return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
|
|
@@ -5616,7 +5935,7 @@ function runSetup(machineId, options = {}) {
|
|
|
5616
5935
|
}
|
|
5617
5936
|
|
|
5618
5937
|
// src/commands/sync.ts
|
|
5619
|
-
import { existsSync as
|
|
5938
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
5620
5939
|
function quote4(value) {
|
|
5621
5940
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5622
5941
|
}
|
|
@@ -5664,8 +5983,8 @@ function detectPackageActions(machine) {
|
|
|
5664
5983
|
}
|
|
5665
5984
|
function detectFileActions(machine) {
|
|
5666
5985
|
return (machine.files || []).map((file, index) => {
|
|
5667
|
-
const sourceExists =
|
|
5668
|
-
const targetExists =
|
|
5986
|
+
const sourceExists = existsSync7(file.source);
|
|
5987
|
+
const targetExists = existsSync7(file.target);
|
|
5669
5988
|
let status = "missing";
|
|
5670
5989
|
if (sourceExists && targetExists) {
|
|
5671
5990
|
if (file.mode === "symlink") {
|
|
@@ -5777,183 +6096,6 @@ function getAgentStatus(machineId = getLocalMachineId()) {
|
|
|
5777
6096
|
}));
|
|
5778
6097
|
}
|
|
5779
6098
|
|
|
5780
|
-
// src/topology.ts
|
|
5781
|
-
import { existsSync as existsSync7 } from "fs";
|
|
5782
|
-
import { arch as arch2, hostname as hostname4, platform as platform3, userInfo as userInfo2 } from "os";
|
|
5783
|
-
import { spawnSync as spawnSync4 } from "child_process";
|
|
5784
|
-
function normalizePlatform2(value = platform3()) {
|
|
5785
|
-
const normalized = value.toLowerCase();
|
|
5786
|
-
if (normalized === "darwin" || normalized === "macos")
|
|
5787
|
-
return "macos";
|
|
5788
|
-
if (normalized === "win32" || normalized === "windows")
|
|
5789
|
-
return "windows";
|
|
5790
|
-
if (normalized === "linux")
|
|
5791
|
-
return "linux";
|
|
5792
|
-
return value;
|
|
5793
|
-
}
|
|
5794
|
-
function defaultRunner(command) {
|
|
5795
|
-
const result = spawnSync4("bash", ["-c", command], {
|
|
5796
|
-
encoding: "utf8",
|
|
5797
|
-
env: process.env
|
|
5798
|
-
});
|
|
5799
|
-
return {
|
|
5800
|
-
stdout: result.stdout || "",
|
|
5801
|
-
stderr: result.stderr || "",
|
|
5802
|
-
exitCode: result.status ?? 1
|
|
5803
|
-
};
|
|
5804
|
-
}
|
|
5805
|
-
function hasCommand2(command, runner) {
|
|
5806
|
-
return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
|
|
5807
|
-
}
|
|
5808
|
-
function parseTailscaleStatus(raw) {
|
|
5809
|
-
try {
|
|
5810
|
-
const parsed = JSON.parse(raw);
|
|
5811
|
-
if (!parsed || typeof parsed !== "object")
|
|
5812
|
-
return null;
|
|
5813
|
-
return parsed;
|
|
5814
|
-
} catch {
|
|
5815
|
-
return null;
|
|
5816
|
-
}
|
|
5817
|
-
}
|
|
5818
|
-
function loadTailscalePeers(runner, warnings) {
|
|
5819
|
-
const peers = new Map;
|
|
5820
|
-
if (!hasCommand2("tailscale", runner)) {
|
|
5821
|
-
warnings.push("tailscale_not_available");
|
|
5822
|
-
return peers;
|
|
5823
|
-
}
|
|
5824
|
-
const result = runner("tailscale status --json");
|
|
5825
|
-
if (result.exitCode !== 0) {
|
|
5826
|
-
warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
|
|
5827
|
-
return peers;
|
|
5828
|
-
}
|
|
5829
|
-
const status = parseTailscaleStatus(result.stdout);
|
|
5830
|
-
if (!status) {
|
|
5831
|
-
warnings.push("tailscale_status_invalid_json");
|
|
5832
|
-
return peers;
|
|
5833
|
-
}
|
|
5834
|
-
const addPeer = (peer) => {
|
|
5835
|
-
if (!peer)
|
|
5836
|
-
return;
|
|
5837
|
-
const id = peer.HostName || peer.DNSName?.split(".")[0];
|
|
5838
|
-
if (id)
|
|
5839
|
-
peers.set(id, peer);
|
|
5840
|
-
};
|
|
5841
|
-
addPeer(status.Self);
|
|
5842
|
-
for (const peer of Object.values(status.Peer ?? {}))
|
|
5843
|
-
addPeer(peer);
|
|
5844
|
-
return peers;
|
|
5845
|
-
}
|
|
5846
|
-
function machineKeys(machine) {
|
|
5847
|
-
return [
|
|
5848
|
-
machine.id,
|
|
5849
|
-
machine.hostname,
|
|
5850
|
-
machine.tailscaleName?.split(".")[0],
|
|
5851
|
-
machine.tailscaleName,
|
|
5852
|
-
machine.sshAddress?.split("@").pop()
|
|
5853
|
-
].filter((value) => Boolean(value));
|
|
5854
|
-
}
|
|
5855
|
-
function findTailscalePeer(machine, machineId, peers) {
|
|
5856
|
-
if (machine) {
|
|
5857
|
-
for (const key of machineKeys(machine)) {
|
|
5858
|
-
const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
|
|
5859
|
-
if (peer)
|
|
5860
|
-
return peer;
|
|
5861
|
-
}
|
|
5862
|
-
}
|
|
5863
|
-
return peers.get(machineId) ?? null;
|
|
5864
|
-
}
|
|
5865
|
-
function routeHints(input) {
|
|
5866
|
-
const hints = [];
|
|
5867
|
-
if (input.machineId === input.localMachineId) {
|
|
5868
|
-
hints.push({ kind: "local", target: "localhost", reachable: true });
|
|
5869
|
-
}
|
|
5870
|
-
if (input.manifest?.sshAddress) {
|
|
5871
|
-
hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: null });
|
|
5872
|
-
}
|
|
5873
|
-
if (input.manifest?.hostname) {
|
|
5874
|
-
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: null });
|
|
5875
|
-
}
|
|
5876
|
-
const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
|
|
5877
|
-
if (tailscaleTarget) {
|
|
5878
|
-
hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
|
|
5879
|
-
}
|
|
5880
|
-
return hints;
|
|
5881
|
-
}
|
|
5882
|
-
function buildEntry(input) {
|
|
5883
|
-
const manifest = input.manifest;
|
|
5884
|
-
const peer = input.peer;
|
|
5885
|
-
const hints = routeHints({
|
|
5886
|
-
machineId: input.machineId,
|
|
5887
|
-
localMachineId: input.localMachineId,
|
|
5888
|
-
manifest,
|
|
5889
|
-
peer
|
|
5890
|
-
});
|
|
5891
|
-
const selectedRoute = hints.find((hint) => hint.kind === "local") ?? hints.find((hint) => hint.kind === "ssh") ?? hints.find((hint) => hint.kind === "lan") ?? hints.find((hint) => hint.kind === "tailscale");
|
|
5892
|
-
const route = selectedRoute?.kind === "ssh" ? "lan" : selectedRoute?.kind ?? "unknown";
|
|
5893
|
-
return {
|
|
5894
|
-
machine_id: input.machineId,
|
|
5895
|
-
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
5896
|
-
platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
|
|
5897
|
-
os: peer?.OS ?? null,
|
|
5898
|
-
user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
|
|
5899
|
-
workspace_path: manifest?.workspacePath ?? null,
|
|
5900
|
-
manifest_declared: Boolean(manifest),
|
|
5901
|
-
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
5902
|
-
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
5903
|
-
tailscale: {
|
|
5904
|
-
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
5905
|
-
ips: peer?.TailscaleIPs ?? [],
|
|
5906
|
-
online: peer?.Online ?? null,
|
|
5907
|
-
active: peer?.Active ?? null,
|
|
5908
|
-
last_seen: peer?.LastSeen ?? null
|
|
5909
|
-
},
|
|
5910
|
-
ssh: {
|
|
5911
|
-
address: manifest?.sshAddress ?? null,
|
|
5912
|
-
route,
|
|
5913
|
-
command_target: selectedRoute?.target ?? null
|
|
5914
|
-
},
|
|
5915
|
-
route_hints: hints,
|
|
5916
|
-
tags: manifest?.tags ?? [],
|
|
5917
|
-
metadata: manifest?.metadata ?? {}
|
|
5918
|
-
};
|
|
5919
|
-
}
|
|
5920
|
-
function discoverMachineTopology(options = {}) {
|
|
5921
|
-
const now = options.now ?? new Date;
|
|
5922
|
-
const runner = options.runner ?? defaultRunner;
|
|
5923
|
-
const warnings = [];
|
|
5924
|
-
const manifest = readManifest();
|
|
5925
|
-
const heartbeats = listHeartbeats();
|
|
5926
|
-
const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
|
|
5927
|
-
const localMachineId = getLocalMachineId();
|
|
5928
|
-
const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
|
|
5929
|
-
const machineIds = new Set([
|
|
5930
|
-
localMachineId,
|
|
5931
|
-
...manifest.machines.map((machine) => machine.id),
|
|
5932
|
-
...heartbeats.map((heartbeat) => heartbeat.machine_id),
|
|
5933
|
-
...peers.keys()
|
|
5934
|
-
]);
|
|
5935
|
-
const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
|
|
5936
|
-
const machines = [...machineIds].sort().map((machineId) => {
|
|
5937
|
-
const manifestMachine = manifestById.get(machineId);
|
|
5938
|
-
return buildEntry({
|
|
5939
|
-
machineId,
|
|
5940
|
-
localMachineId,
|
|
5941
|
-
manifest: manifestMachine,
|
|
5942
|
-
peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
|
|
5943
|
-
heartbeat: heartbeatByMachine.get(machineId)
|
|
5944
|
-
});
|
|
5945
|
-
});
|
|
5946
|
-
return {
|
|
5947
|
-
generated_at: now.toISOString(),
|
|
5948
|
-
local_machine_id: localMachineId,
|
|
5949
|
-
local_hostname: hostname4(),
|
|
5950
|
-
current_platform: normalizePlatform2(),
|
|
5951
|
-
manifest_path_known: existsSync7(getManifestPath()),
|
|
5952
|
-
machines,
|
|
5953
|
-
warnings
|
|
5954
|
-
};
|
|
5955
|
-
}
|
|
5956
|
-
|
|
5957
6099
|
// src/compatibility.ts
|
|
5958
6100
|
var DEFAULT_COMMANDS = [
|
|
5959
6101
|
{ command: "bun", required: true },
|
|
@@ -5962,7 +6104,7 @@ var DEFAULT_COMMANDS = [
|
|
|
5962
6104
|
function defaultPackages() {
|
|
5963
6105
|
return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
|
|
5964
6106
|
}
|
|
5965
|
-
function
|
|
6107
|
+
function shellQuote5(value) {
|
|
5966
6108
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
5967
6109
|
}
|
|
5968
6110
|
function commandId(value) {
|
|
@@ -6013,7 +6155,7 @@ function defaultRunner2(machineId, command) {
|
|
|
6013
6155
|
return runMachineCommand(machineId, command);
|
|
6014
6156
|
}
|
|
6015
6157
|
function inspectCommand(machineId, spec, runner) {
|
|
6016
|
-
const command =
|
|
6158
|
+
const command = shellQuote5(spec.command);
|
|
6017
6159
|
const versionArgs = spec.versionArgs ?? "--version";
|
|
6018
6160
|
const script = [
|
|
6019
6161
|
`cmd=${command}`,
|
|
@@ -6042,7 +6184,7 @@ function fieldCommand(field) {
|
|
|
6042
6184
|
}
|
|
6043
6185
|
function inspectWorkspace(machineId, spec, runner) {
|
|
6044
6186
|
const script = [
|
|
6045
|
-
`path=${
|
|
6187
|
+
`path=${shellQuote5(spec.path)}`,
|
|
6046
6188
|
'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
|
|
6047
6189
|
'pkg="$path/package.json"',
|
|
6048
6190
|
'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
|
|
@@ -6180,6 +6322,17 @@ function checkMachineCompatibility(options = {}) {
|
|
|
6180
6322
|
fail: checks.filter((check2) => check2.status === "fail").length
|
|
6181
6323
|
};
|
|
6182
6324
|
return {
|
|
6325
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
6326
|
+
package: {
|
|
6327
|
+
name: MACHINES_PACKAGE_NAME,
|
|
6328
|
+
version: getPackageVersion()
|
|
6329
|
+
},
|
|
6330
|
+
capabilities: {
|
|
6331
|
+
topology: true,
|
|
6332
|
+
compatibility: true,
|
|
6333
|
+
route_resolution: true,
|
|
6334
|
+
cli_json_fallback: true
|
|
6335
|
+
},
|
|
6183
6336
|
ok: summary.fail === 0,
|
|
6184
6337
|
machine_id: machineId,
|
|
6185
6338
|
source: checks[0]?.source ?? "local",
|
|
@@ -6599,8 +6752,11 @@ function createMcpServer(version) {
|
|
|
6599
6752
|
}));
|
|
6600
6753
|
server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }] }));
|
|
6601
6754
|
server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }] }));
|
|
6755
|
+
server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", { machine_id: exports_external.string().describe("Machine identifier"), include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json") }, async ({ machine_id, include_tailscale }) => ({
|
|
6756
|
+
content: [{ type: "text", text: JSON.stringify(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), null, 2) }]
|
|
6757
|
+
}));
|
|
6602
6758
|
server.tool("machines_ssh_resolve", "Resolve the best SSH route for a machine.", { machine_id: exports_external.string().describe("Machine identifier"), remote_command: exports_external.string().optional().describe("Optional remote command") }, async ({ machine_id, remote_command }) => ({
|
|
6603
|
-
content: [{ type: "text", text: JSON.stringify({ resolved:
|
|
6759
|
+
content: [{ type: "text", text: JSON.stringify({ resolved: resolveMachineRoute(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
|
|
6604
6760
|
}));
|
|
6605
6761
|
server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
|
|
6606
6762
|
content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
|