@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/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 hostname3 } from "os";
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/commands/ssh.ts
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 isReachable(host) {
4413
+ function manifestHostReachable(target) {
4329
4414
  const overrides = envReachableHosts();
4330
- if (overrides.size > 0) {
4331
- return overrides.has(host);
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
- const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
4334
- stdio: "ignore"
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
- return probe.status === 0;
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 resolveSshTarget(machineId) {
4339
- const machine = getManifestMachine(machineId);
4340
- if (!machine) {
4341
- throw new Error(`Machine not found in manifest: ${machineId}`);
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 current = detectCurrentMachineManifest();
4344
- if (machine.id === current.id) {
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
- machineId,
4347
- target: "localhost",
4348
- route: "local"
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 lanTarget = machine.sshAddress || machine.hostname || machine.id;
4352
- const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
4353
- const route = isReachable(lanTarget) ? "lan" : "tailscale";
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
- machineId,
4356
- target: route === "lan" ? lanTarget : tailscaleTarget,
4357
- route
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
- function buildSshCommand(machineId, remoteCommand) {
4361
- const resolved = resolveSshTarget(machineId);
4362
- return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
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 shellQuote(value) {
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 === hostname3();
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
- if (String(error.message ?? error).includes("Machine not found in manifest")) {
4383
- return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
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 shellQuote2(value) {
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 = shellQuote2(getPackageName(app));
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 platform2 } from "os";
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 (platform2() === "darwin") {
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 existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
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 (!existsSync4(path))
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 existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
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 shellQuote3(value) {
5266
+ function shellQuote4(value) {
4948
5267
  return `'${value.replace(/'/g, `'\\''`)}'`;
4949
5268
  }
4950
- function hasCommand(binary) {
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 (hasCommand("sendmail")) {
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 (hasCommand("mail")) {
4994
- const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
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 (!existsSync5(path)) {
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 existsSync6, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
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 = existsSync6(file.source);
5668
- const targetExists = existsSync6(file.target);
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 shellQuote4(value) {
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 = shellQuote4(spec.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=${shellQuote4(spec.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: resolveSshTarget(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
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) }]