@hasna/machines 0.0.16 → 0.0.17

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,46 +4319,361 @@ 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) });
4426
+ }
4427
+ if (input.manifest?.hostname) {
4428
+ hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
4332
4429
  }
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"
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
4655
+ };
4656
+ }
4657
+
4658
+ // src/commands/ssh.ts
4659
+ function resolveSshTarget(machineId, options = {}) {
4660
+ const resolved = resolveMachineRoute(machineId, options);
4661
+ if (!resolved.ok || !resolved.target) {
4662
+ throw new Error(`Machine route not found: ${machineId}`);
4663
+ }
4664
+ if (resolved.route !== "local" && resolved.route !== "lan" && resolved.route !== "tailscale" && resolved.route !== "ssh") {
4665
+ throw new Error(`Machine route is not SSH-capable: ${machineId}`);
4666
+ }
4667
+ return {
4668
+ machineId: resolved.machine_id ?? machineId,
4669
+ target: resolved.target,
4670
+ route: resolved.route,
4671
+ confidence: resolved.confidence,
4672
+ warnings: resolved.warnings
4358
4673
  };
4359
4674
  }
4360
- function buildSshCommand(machineId, remoteCommand) {
4361
- const resolved = resolveSshTarget(machineId);
4675
+ function buildSshCommand(machineId, remoteCommand, options = {}) {
4676
+ const resolved = resolveSshTarget(machineId, options);
4362
4677
  return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
4363
4678
  }
4364
4679
 
@@ -4367,7 +4682,7 @@ function shellQuote(value) {
4367
4682
  return `'${value.replace(/'/g, "'\\''")}'`;
4368
4683
  }
4369
4684
  function machineIsLocal(machineId, localMachineId) {
4370
- return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname3();
4685
+ return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname4();
4371
4686
  }
4372
4687
  function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
4373
4688
  if (machineIsLocal(machineId, localMachineId)) {
@@ -4379,7 +4694,8 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
4379
4694
  shellCommand: buildSshCommand(machineId, command)
4380
4695
  };
4381
4696
  } catch (error) {
4382
- if (String(error.message ?? error).includes("Machine not found in manifest")) {
4697
+ const message = String(error.message ?? error);
4698
+ if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
4383
4699
  return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
4384
4700
  }
4385
4701
  throw error;
@@ -4539,7 +4855,7 @@ function runAppsInstall(machineId, options = {}) {
4539
4855
  }
4540
4856
 
4541
4857
  // src/commands/cert.ts
4542
- import { homedir as homedir3, platform as platform2 } from "os";
4858
+ import { homedir as homedir3, platform as platform3 } from "os";
4543
4859
  import { join as join4 } from "path";
4544
4860
  function quote2(value) {
4545
4861
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -4555,7 +4871,7 @@ function buildCertPlan(domains) {
4555
4871
  const certPath = join4(certDir(), `${primary}.pem`);
4556
4872
  const keyPath = join4(certDir(), `${primary}-key.pem`);
4557
4873
  const steps = [];
4558
- if (platform2() === "darwin") {
4874
+ if (platform3() === "darwin") {
4559
4875
  steps.push({
4560
4876
  id: "mkcert-install-macos",
4561
4877
  title: "Install mkcert on macOS",
@@ -4617,14 +4933,14 @@ function runCertPlan(domains, options = {}) {
4617
4933
  }
4618
4934
 
4619
4935
  // src/commands/dns.ts
4620
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4936
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4621
4937
  import { join as join5 } from "path";
4622
4938
  function getDnsPath() {
4623
4939
  return join5(getDataDir(), "dns.json");
4624
4940
  }
4625
4941
  function readMappings() {
4626
4942
  const path = getDnsPath();
4627
- if (!existsSync4(path))
4943
+ if (!existsSync5(path))
4628
4944
  return [];
4629
4945
  return JSON.parse(readFileSync3(path, "utf8"));
4630
4946
  }
@@ -4928,7 +5244,7 @@ function runTailscaleInstall(machineId, options = {}) {
4928
5244
  }
4929
5245
 
4930
5246
  // src/commands/notifications.ts
4931
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5247
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4932
5248
  var notificationChannelSchema = exports_external.object({
4933
5249
  id: exports_external.string(),
4934
5250
  type: exports_external.enum(["email", "webhook", "command"]),
@@ -4947,7 +5263,7 @@ function sortChannels(channels) {
4947
5263
  function shellQuote3(value) {
4948
5264
  return `'${value.replace(/'/g, `'\\''`)}'`;
4949
5265
  }
4950
- function hasCommand(binary) {
5266
+ function hasCommand2(binary) {
4951
5267
  const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
4952
5268
  stdout: "ignore",
4953
5269
  stderr: "ignore",
@@ -4972,7 +5288,7 @@ Content-Type: text/plain; charset=utf-8
4972
5288
 
4973
5289
  ${message}
4974
5290
  `;
4975
- if (hasCommand("sendmail")) {
5291
+ if (hasCommand2("sendmail")) {
4976
5292
  const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
4977
5293
  stdin: new TextEncoder().encode(body),
4978
5294
  stdout: "pipe",
@@ -4990,7 +5306,7 @@ ${message}
4990
5306
  detail: `Delivered via sendmail to ${channel.target}`
4991
5307
  };
4992
5308
  }
4993
- if (hasCommand("mail")) {
5309
+ if (hasCommand2("mail")) {
4994
5310
  const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
4995
5311
  const result = Bun.spawnSync(["bash", "-lc", command], {
4996
5312
  stdout: "pipe",
@@ -5084,7 +5400,7 @@ function getDefaultNotificationConfig() {
5084
5400
  };
5085
5401
  }
5086
5402
  function readNotificationConfig(path = getNotificationsPath()) {
5087
- if (!existsSync5(path)) {
5403
+ if (!existsSync6(path)) {
5088
5404
  return getDefaultNotificationConfig();
5089
5405
  }
5090
5406
  return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
@@ -5616,7 +5932,7 @@ function runSetup(machineId, options = {}) {
5616
5932
  }
5617
5933
 
5618
5934
  // src/commands/sync.ts
5619
- import { existsSync as existsSync6, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
5935
+ import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
5620
5936
  function quote4(value) {
5621
5937
  return `'${value.replace(/'/g, `'\\''`)}'`;
5622
5938
  }
@@ -5664,8 +5980,8 @@ function detectPackageActions(machine) {
5664
5980
  }
5665
5981
  function detectFileActions(machine) {
5666
5982
  return (machine.files || []).map((file, index) => {
5667
- const sourceExists = existsSync6(file.source);
5668
- const targetExists = existsSync6(file.target);
5983
+ const sourceExists = existsSync7(file.source);
5984
+ const targetExists = existsSync7(file.target);
5669
5985
  let status = "missing";
5670
5986
  if (sourceExists && targetExists) {
5671
5987
  if (file.mode === "symlink") {
@@ -5777,183 +6093,6 @@ function getAgentStatus(machineId = getLocalMachineId()) {
5777
6093
  }));
5778
6094
  }
5779
6095
 
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
6096
  // src/compatibility.ts
5958
6097
  var DEFAULT_COMMANDS = [
5959
6098
  { command: "bun", required: true },
@@ -6180,6 +6319,17 @@ function checkMachineCompatibility(options = {}) {
6180
6319
  fail: checks.filter((check2) => check2.status === "fail").length
6181
6320
  };
6182
6321
  return {
6322
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
6323
+ package: {
6324
+ name: MACHINES_PACKAGE_NAME,
6325
+ version: getPackageVersion()
6326
+ },
6327
+ capabilities: {
6328
+ topology: true,
6329
+ compatibility: true,
6330
+ route_resolution: true,
6331
+ cli_json_fallback: true
6332
+ },
6183
6333
  ok: summary.fail === 0,
6184
6334
  machine_id: machineId,
6185
6335
  source: checks[0]?.source ?? "local",
@@ -6599,8 +6749,11 @@ function createMcpServer(version) {
6599
6749
  }));
6600
6750
  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
6751
  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) }] }));
6752
+ 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 }) => ({
6753
+ content: [{ type: "text", text: JSON.stringify(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), null, 2) }]
6754
+ }));
6602
6755
  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) }]
6756
+ content: [{ type: "text", text: JSON.stringify({ resolved: resolveMachineRoute(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
6604
6757
  }));
6605
6758
  server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
6606
6759
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]