@hasna/machines 0.0.14 → 0.0.15

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
@@ -16,11 +16,30 @@ var __export = (target, all) => {
16
16
  };
17
17
 
18
18
  // src/mcp/index.ts
19
- import { readFileSync as readFileSync6 } from "fs";
20
- import { dirname as dirname4, join as join6 } from "path";
21
- import { fileURLToPath as fileURLToPath2 } from "url";
22
19
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
20
 
21
+ // src/version.ts
22
+ import { existsSync, readFileSync } from "fs";
23
+ import { dirname, join } from "path";
24
+ import { fileURLToPath } from "url";
25
+ function getPackageVersion() {
26
+ try {
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
29
+ const pkgPath = candidates.find((candidate) => existsSync(candidate));
30
+ if (!pkgPath) {
31
+ return "0.0.0";
32
+ }
33
+ return JSON.parse(readFileSync(pkgPath, "utf8")).version || "0.0.0";
34
+ } catch {
35
+ return "0.0.0";
36
+ }
37
+ }
38
+
39
+ // src/mcp/http.ts
40
+ import { createServer } from "http";
41
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
42
+
24
43
  // src/mcp/server.ts
25
44
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26
45
 
@@ -3999,20 +4018,20 @@ var coerce = {
3999
4018
  var NEVER = INVALID;
4000
4019
  // src/commands/backup.ts
4001
4020
  import { homedir } from "os";
4002
- import { join } from "path";
4021
+ import { join as join2 } from "path";
4003
4022
  function quote(value) {
4004
4023
  return `'${value.replace(/'/g, `'\\''`)}'`;
4005
4024
  }
4006
4025
  function defaultBackupSources() {
4007
4026
  const home = homedir();
4008
4027
  return [
4009
- join(home, ".hasna"),
4010
- join(home, ".ssh"),
4011
- join(home, ".secrets")
4028
+ join2(home, ".hasna"),
4029
+ join2(home, ".ssh"),
4030
+ join2(home, ".secrets")
4012
4031
  ];
4013
4032
  }
4014
4033
  function buildBackupPlan(bucket, prefix = "machines") {
4015
- const archivePath = join(homedir(), ".hasna", "machines", "backup.tgz");
4034
+ const archivePath = join2(homedir(), ".hasna", "machines", "backup.tgz");
4016
4035
  const sources = defaultBackupSources();
4017
4036
  const steps = [
4018
4037
  {
@@ -4063,33 +4082,33 @@ function runBackup(bucket, prefix = "machines", options = {}) {
4063
4082
  }
4064
4083
 
4065
4084
  // src/manifests.ts
4066
- import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
4085
+ import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
4067
4086
  import { arch, homedir as homedir2, hostname, platform, userInfo } from "os";
4068
- import { dirname as dirname2 } from "path";
4087
+ import { dirname as dirname3 } from "path";
4069
4088
 
4070
4089
  // src/paths.ts
4071
- import { existsSync, mkdirSync } from "fs";
4072
- import { dirname, join as join2, resolve } from "path";
4090
+ import { existsSync as existsSync2, mkdirSync } from "fs";
4091
+ import { dirname as dirname2, join as join3, resolve } from "path";
4073
4092
  function homeDir() {
4074
4093
  return process.env["HOME"] || process.env["USERPROFILE"] || "~";
4075
4094
  }
4076
4095
  function getDataDir() {
4077
- return process.env["HASNA_MACHINES_DIR"] || join2(homeDir(), ".hasna", "machines");
4096
+ return process.env["HASNA_MACHINES_DIR"] || join3(homeDir(), ".hasna", "machines");
4078
4097
  }
4079
4098
  function getDbPath() {
4080
- return process.env["HASNA_MACHINES_DB_PATH"] || join2(getDataDir(), "machines.db");
4099
+ return process.env["HASNA_MACHINES_DB_PATH"] || join3(getDataDir(), "machines.db");
4081
4100
  }
4082
4101
  function getManifestPath() {
4083
- return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join2(getDataDir(), "machines.json");
4102
+ return process.env["HASNA_MACHINES_MANIFEST_PATH"] || join3(getDataDir(), "machines.json");
4084
4103
  }
4085
4104
  function getNotificationsPath() {
4086
- return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join2(getDataDir(), "notifications.json");
4105
+ return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join3(getDataDir(), "notifications.json");
4087
4106
  }
4088
4107
  function ensureParentDir(filePath) {
4089
4108
  if (filePath === ":memory:")
4090
4109
  return;
4091
- const dir = dirname(resolve(filePath));
4092
- if (!existsSync(dir)) {
4110
+ const dir = dirname2(resolve(filePath));
4111
+ if (!existsSync2(dir)) {
4093
4112
  mkdirSync(dir, { recursive: true });
4094
4113
  }
4095
4114
  }
@@ -4154,10 +4173,10 @@ function getDefaultManifest() {
4154
4173
  };
4155
4174
  }
4156
4175
  function readManifest(path = getManifestPath()) {
4157
- if (!existsSync2(path)) {
4176
+ if (!existsSync3(path)) {
4158
4177
  return getDefaultManifest();
4159
4178
  }
4160
- const raw = JSON.parse(readFileSync(path, "utf8"));
4179
+ const raw = JSON.parse(readFileSync2(path, "utf8"));
4161
4180
  return fleetSchema.parse(raw);
4162
4181
  }
4163
4182
  function validateManifest(path = getManifestPath()) {
@@ -4181,7 +4200,7 @@ function getManifestMachine(machineId, path = getManifestPath()) {
4181
4200
  function detectCurrentMachineManifest() {
4182
4201
  const machineId = process.env["HASNA_MACHINES_MACHINE_ID"] || hostname();
4183
4202
  const user = userInfo().username;
4184
- const bunDir = dirname2(process.execPath);
4203
+ const bunDir = dirname3(process.execPath);
4185
4204
  return {
4186
4205
  id: machineId,
4187
4206
  hostname: hostname(),
@@ -4348,7 +4367,7 @@ function runMachineCommand(machineId, command) {
4348
4367
  const isLocal = machineId === localMachineId;
4349
4368
  const route = isLocal ? "local" : resolveSshTarget(machineId).route;
4350
4369
  const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
4351
- const result = spawnSync2("bash", ["-lc", shellCommand], {
4370
+ const result = spawnSync2("bash", ["-c", shellCommand], {
4352
4371
  encoding: "utf8",
4353
4372
  env: process.env
4354
4373
  });
@@ -4501,20 +4520,20 @@ function runAppsInstall(machineId, options = {}) {
4501
4520
 
4502
4521
  // src/commands/cert.ts
4503
4522
  import { homedir as homedir3, platform as platform2 } from "os";
4504
- import { join as join3 } from "path";
4523
+ import { join as join4 } from "path";
4505
4524
  function quote2(value) {
4506
4525
  return `'${value.replace(/'/g, `'\\''`)}'`;
4507
4526
  }
4508
4527
  function certDir() {
4509
- return join3(homedir3(), ".hasna", "machines", "certs");
4528
+ return join4(homedir3(), ".hasna", "machines", "certs");
4510
4529
  }
4511
4530
  function buildCertPlan(domains) {
4512
4531
  if (domains.length === 0) {
4513
4532
  throw new Error("At least one domain is required.");
4514
4533
  }
4515
4534
  const primary = domains[0];
4516
- const certPath = join3(certDir(), `${primary}.pem`);
4517
- const keyPath = join3(certDir(), `${primary}-key.pem`);
4535
+ const certPath = join4(certDir(), `${primary}.pem`);
4536
+ const keyPath = join4(certDir(), `${primary}-key.pem`);
4518
4537
  const steps = [];
4519
4538
  if (platform2() === "darwin") {
4520
4539
  steps.push({
@@ -4578,16 +4597,16 @@ function runCertPlan(domains, options = {}) {
4578
4597
  }
4579
4598
 
4580
4599
  // src/commands/dns.ts
4581
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4582
- import { join as join4 } from "path";
4600
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4601
+ import { join as join5 } from "path";
4583
4602
  function getDnsPath() {
4584
- return join4(getDataDir(), "dns.json");
4603
+ return join5(getDataDir(), "dns.json");
4585
4604
  }
4586
4605
  function readMappings() {
4587
4606
  const path = getDnsPath();
4588
- if (!existsSync3(path))
4607
+ if (!existsSync4(path))
4589
4608
  return [];
4590
- return JSON.parse(readFileSync2(path, "utf8"));
4609
+ return JSON.parse(readFileSync3(path, "utf8"));
4591
4610
  }
4592
4611
  function writeMappings(mappings) {
4593
4612
  const path = getDnsPath();
@@ -4614,10 +4633,10 @@ function renderDomainMapping(domain) {
4614
4633
  hostsEntry: `${entry.targetHost} ${entry.domain}`,
4615
4634
  caddySnippet: `${entry.domain} {
4616
4635
  reverse_proxy 127.0.0.1:${entry.port}
4617
- tls ${join4(getDataDir(), "certs", `${entry.domain}.pem`)} ${join4(getDataDir(), "certs", `${entry.domain}-key.pem`)}
4636
+ tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
4618
4637
  }`,
4619
- certPath: join4(getDataDir(), "certs", `${entry.domain}.pem`),
4620
- keyPath: join4(getDataDir(), "certs", `${entry.domain}-key.pem`)
4638
+ certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
4639
+ keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
4621
4640
  };
4622
4641
  }
4623
4642
 
@@ -4889,7 +4908,7 @@ function runTailscaleInstall(machineId, options = {}) {
4889
4908
  }
4890
4909
 
4891
4910
  // src/commands/notifications.ts
4892
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4911
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4893
4912
  var notificationChannelSchema = exports_external.object({
4894
4913
  id: exports_external.string(),
4895
4914
  type: exports_external.enum(["email", "webhook", "command"]),
@@ -5045,10 +5064,10 @@ function getDefaultNotificationConfig() {
5045
5064
  };
5046
5065
  }
5047
5066
  function readNotificationConfig(path = getNotificationsPath()) {
5048
- if (!existsSync4(path)) {
5067
+ if (!existsSync5(path)) {
5049
5068
  return getDefaultNotificationConfig();
5050
5069
  }
5051
- return notificationConfigSchema.parse(JSON.parse(readFileSync3(path, "utf8")));
5070
+ return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
5052
5071
  }
5053
5072
  function writeNotificationConfig(config, path = getNotificationsPath()) {
5054
5073
  ensureParentDir(path);
@@ -5220,24 +5239,6 @@ function manifestValidate() {
5220
5239
  return validateManifest(getManifestPath());
5221
5240
  }
5222
5241
 
5223
- // src/version.ts
5224
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
5225
- import { dirname as dirname3, join as join5 } from "path";
5226
- import { fileURLToPath } from "url";
5227
- function getPackageVersion() {
5228
- try {
5229
- const here = dirname3(fileURLToPath(import.meta.url));
5230
- const candidates = [join5(here, "..", "package.json"), join5(here, "..", "..", "package.json")];
5231
- const pkgPath = candidates.find((candidate) => existsSync5(candidate));
5232
- if (!pkgPath) {
5233
- return "0.0.0";
5234
- }
5235
- return JSON.parse(readFileSync4(pkgPath, "utf8")).version || "0.0.0";
5236
- } catch {
5237
- return "0.0.0";
5238
- }
5239
- }
5240
-
5241
5242
  // src/commands/status.ts
5242
5243
  function getStatus() {
5243
5244
  const manifest = readManifest();
@@ -5756,7 +5757,742 @@ function getAgentStatus(machineId = getLocalMachineId()) {
5756
5757
  }));
5757
5758
  }
5758
5759
 
5760
+ // src/topology.ts
5761
+ import { existsSync as existsSync7 } from "fs";
5762
+ import { arch as arch2, hostname as hostname3, platform as platform3, userInfo as userInfo2 } from "os";
5763
+ import { spawnSync as spawnSync4 } from "child_process";
5764
+ function normalizePlatform2(value = platform3()) {
5765
+ const normalized = value.toLowerCase();
5766
+ if (normalized === "darwin" || normalized === "macos")
5767
+ return "macos";
5768
+ if (normalized === "win32" || normalized === "windows")
5769
+ return "windows";
5770
+ if (normalized === "linux")
5771
+ return "linux";
5772
+ return value;
5773
+ }
5774
+ function defaultRunner(command) {
5775
+ const result = spawnSync4("bash", ["-c", command], {
5776
+ encoding: "utf8",
5777
+ env: process.env
5778
+ });
5779
+ return {
5780
+ stdout: result.stdout || "",
5781
+ stderr: result.stderr || "",
5782
+ exitCode: result.status ?? 1
5783
+ };
5784
+ }
5785
+ function hasCommand2(command, runner) {
5786
+ return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
5787
+ }
5788
+ function parseTailscaleStatus(raw) {
5789
+ try {
5790
+ const parsed = JSON.parse(raw);
5791
+ if (!parsed || typeof parsed !== "object")
5792
+ return null;
5793
+ return parsed;
5794
+ } catch {
5795
+ return null;
5796
+ }
5797
+ }
5798
+ function loadTailscalePeers(runner, warnings) {
5799
+ const peers = new Map;
5800
+ if (!hasCommand2("tailscale", runner)) {
5801
+ warnings.push("tailscale_not_available");
5802
+ return peers;
5803
+ }
5804
+ const result = runner("tailscale status --json");
5805
+ if (result.exitCode !== 0) {
5806
+ warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
5807
+ return peers;
5808
+ }
5809
+ const status = parseTailscaleStatus(result.stdout);
5810
+ if (!status) {
5811
+ warnings.push("tailscale_status_invalid_json");
5812
+ return peers;
5813
+ }
5814
+ const addPeer = (peer) => {
5815
+ if (!peer)
5816
+ return;
5817
+ const id = peer.HostName || peer.DNSName?.split(".")[0];
5818
+ if (id)
5819
+ peers.set(id, peer);
5820
+ };
5821
+ addPeer(status.Self);
5822
+ for (const peer of Object.values(status.Peer ?? {}))
5823
+ addPeer(peer);
5824
+ return peers;
5825
+ }
5826
+ function machineKeys(machine) {
5827
+ return [
5828
+ machine.id,
5829
+ machine.hostname,
5830
+ machine.tailscaleName?.split(".")[0],
5831
+ machine.tailscaleName,
5832
+ machine.sshAddress?.split("@").pop()
5833
+ ].filter((value) => Boolean(value));
5834
+ }
5835
+ function findTailscalePeer(machine, machineId, peers) {
5836
+ if (machine) {
5837
+ for (const key of machineKeys(machine)) {
5838
+ const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
5839
+ if (peer)
5840
+ return peer;
5841
+ }
5842
+ }
5843
+ return peers.get(machineId) ?? null;
5844
+ }
5845
+ function routeHints(input) {
5846
+ const hints = [];
5847
+ if (input.machineId === input.localMachineId) {
5848
+ hints.push({ kind: "local", target: "localhost", reachable: true });
5849
+ }
5850
+ if (input.manifest?.sshAddress) {
5851
+ hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: null });
5852
+ }
5853
+ if (input.manifest?.hostname) {
5854
+ hints.push({ kind: "lan", target: input.manifest.hostname, reachable: null });
5855
+ }
5856
+ const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
5857
+ if (tailscaleTarget) {
5858
+ hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
5859
+ }
5860
+ return hints;
5861
+ }
5862
+ function buildEntry(input) {
5863
+ const manifest = input.manifest;
5864
+ const peer = input.peer;
5865
+ const hints = routeHints({
5866
+ machineId: input.machineId,
5867
+ localMachineId: input.localMachineId,
5868
+ manifest,
5869
+ peer
5870
+ });
5871
+ 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");
5872
+ const route = selectedRoute?.kind === "ssh" ? "lan" : selectedRoute?.kind ?? "unknown";
5873
+ return {
5874
+ machine_id: input.machineId,
5875
+ hostname: manifest?.hostname ?? peer?.HostName ?? null,
5876
+ platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
5877
+ os: peer?.OS ?? null,
5878
+ user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
5879
+ workspace_path: manifest?.workspacePath ?? null,
5880
+ manifest_declared: Boolean(manifest),
5881
+ heartbeat_status: input.heartbeat?.status ?? "unknown",
5882
+ last_heartbeat_at: input.heartbeat?.updated_at ?? null,
5883
+ tailscale: {
5884
+ dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
5885
+ ips: peer?.TailscaleIPs ?? [],
5886
+ online: peer?.Online ?? null,
5887
+ active: peer?.Active ?? null,
5888
+ last_seen: peer?.LastSeen ?? null
5889
+ },
5890
+ ssh: {
5891
+ address: manifest?.sshAddress ?? null,
5892
+ route,
5893
+ command_target: selectedRoute?.target ?? null
5894
+ },
5895
+ route_hints: hints,
5896
+ tags: manifest?.tags ?? [],
5897
+ metadata: manifest?.metadata ?? {}
5898
+ };
5899
+ }
5900
+ function discoverMachineTopology(options = {}) {
5901
+ const now = options.now ?? new Date;
5902
+ const runner = options.runner ?? defaultRunner;
5903
+ const warnings = [];
5904
+ const manifest = readManifest();
5905
+ const heartbeats = listHeartbeats();
5906
+ const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
5907
+ const localMachineId = getLocalMachineId();
5908
+ const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
5909
+ const machineIds = new Set([
5910
+ localMachineId,
5911
+ ...manifest.machines.map((machine) => machine.id),
5912
+ ...heartbeats.map((heartbeat) => heartbeat.machine_id),
5913
+ ...peers.keys()
5914
+ ]);
5915
+ const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
5916
+ const machines = [...machineIds].sort().map((machineId) => {
5917
+ const manifestMachine = manifestById.get(machineId);
5918
+ return buildEntry({
5919
+ machineId,
5920
+ localMachineId,
5921
+ manifest: manifestMachine,
5922
+ peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
5923
+ heartbeat: heartbeatByMachine.get(machineId)
5924
+ });
5925
+ });
5926
+ return {
5927
+ generated_at: now.toISOString(),
5928
+ local_machine_id: localMachineId,
5929
+ local_hostname: hostname3(),
5930
+ current_platform: normalizePlatform2(),
5931
+ manifest_path_known: existsSync7(getManifestPath()),
5932
+ machines,
5933
+ warnings
5934
+ };
5935
+ }
5936
+
5937
+ // src/compatibility.ts
5938
+ var DEFAULT_COMMANDS = [
5939
+ { command: "bun", required: true },
5940
+ { command: "machines", required: true }
5941
+ ];
5942
+ function defaultPackages() {
5943
+ return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
5944
+ }
5945
+ function shellQuote3(value) {
5946
+ return `'${value.replace(/'/g, "'\\''")}'`;
5947
+ }
5948
+ function commandId(value) {
5949
+ return value.replace(/[^a-zA-Z0-9_.@/-]+/g, "-").replace(/^-+|-+$/g, "");
5950
+ }
5951
+ function packageCommand(name) {
5952
+ if (name === "@hasna/knowledge")
5953
+ return "knowledge";
5954
+ if (name === "@hasna/machines")
5955
+ return "machines";
5956
+ return name.split("/").pop() ?? name;
5957
+ }
5958
+ function firstLine(value) {
5959
+ return value.trim().split(/\r?\n/).find(Boolean) ?? "";
5960
+ }
5961
+ function extractVersion(value) {
5962
+ const match = value.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
5963
+ return match?.[0] ?? null;
5964
+ }
5965
+ function statusFor(required, ok) {
5966
+ if (ok)
5967
+ return "ok";
5968
+ return required === false ? "warn" : "fail";
5969
+ }
5970
+ function makeCheck2(input) {
5971
+ return {
5972
+ id: input.id,
5973
+ kind: input.kind,
5974
+ status: input.status,
5975
+ target: input.target,
5976
+ expected: input.expected ?? null,
5977
+ actual: input.actual ?? null,
5978
+ detail: input.detail,
5979
+ source: input.source
5980
+ };
5981
+ }
5982
+ function parseKeyValue(stdout) {
5983
+ const result = {};
5984
+ for (const line of stdout.split(/\r?\n/)) {
5985
+ const idx = line.indexOf("=");
5986
+ if (idx <= 0)
5987
+ continue;
5988
+ result[line.slice(0, idx)] = line.slice(idx + 1);
5989
+ }
5990
+ return result;
5991
+ }
5992
+ function defaultRunner2(machineId, command) {
5993
+ return runMachineCommand(machineId, command);
5994
+ }
5995
+ function inspectCommand(machineId, spec, runner) {
5996
+ const command = shellQuote3(spec.command);
5997
+ const versionArgs = spec.versionArgs ?? "--version";
5998
+ const script = [
5999
+ `cmd=${command}`,
6000
+ 'path="$(command -v "$cmd" 2>/dev/null || true)"',
6001
+ 'printf "path=%s\\n" "$path"',
6002
+ 'if [ -n "$path" ]; then version="$("$cmd" ' + versionArgs + ' 2>/dev/null || true)"; printf "version=%s\\n" "$version"; fi'
6003
+ ].join("; ");
6004
+ const result = runner(machineId, script);
6005
+ const parsed = parseKeyValue(result.stdout);
6006
+ return {
6007
+ path: parsed.path || null,
6008
+ version: parsed.version ? firstLine(parsed.version) : null,
6009
+ exitCode: result.exitCode,
6010
+ source: result.source,
6011
+ stderr: result.stderr
6012
+ };
6013
+ }
6014
+ function fieldCommand(field) {
6015
+ const regex = field === "name" ? String.raw`s/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p` : String.raw`s/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p`;
6016
+ return [
6017
+ `if command -v bun >/dev/null 2>&1; then bun -e "const p=JSON.parse(await Bun.file(process.argv[1]).text()); console.log(p.${field} ?? '')" "$pkg" 2>/dev/null`,
6018
+ `elif command -v node >/dev/null 2>&1; then node -e "const fs=require('fs'); const p=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); console.log(p.${field} || '')" "$pkg" 2>/dev/null`,
6019
+ `else sed -n '${regex}' "$pkg" | head -n 1`,
6020
+ "fi"
6021
+ ].join("; ");
6022
+ }
6023
+ function inspectWorkspace(machineId, spec, runner) {
6024
+ const script = [
6025
+ `path=${shellQuote3(spec.path)}`,
6026
+ 'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
6027
+ 'pkg="$path/package.json"',
6028
+ 'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
6029
+ `if [ -f "$pkg" ]; then printf "package_name=%s\\n" "$(${fieldCommand("name")})"; printf "version=%s\\n" "$(${fieldCommand("version")})"; fi`
6030
+ ].join("; ");
6031
+ const result = runner(machineId, script);
6032
+ const parsed = parseKeyValue(result.stdout);
6033
+ return {
6034
+ exists: parsed.exists === "yes",
6035
+ packageJson: parsed.package_json === "yes",
6036
+ packageName: parsed.package_name || null,
6037
+ version: parsed.version || null,
6038
+ source: result.source,
6039
+ stderr: result.stderr
6040
+ };
6041
+ }
6042
+ function commandCheck(machineId, spec, runner) {
6043
+ const inspection = inspectCommand(machineId, spec, runner);
6044
+ const found = Boolean(inspection.path);
6045
+ const checks = [
6046
+ makeCheck2({
6047
+ id: `command:${commandId(spec.command)}:path`,
6048
+ kind: "command",
6049
+ status: statusFor(spec.required, found),
6050
+ target: spec.command,
6051
+ expected: "available",
6052
+ actual: inspection.path ?? "missing",
6053
+ detail: found ? `found at ${inspection.path}` : inspection.stderr || "command missing",
6054
+ source: inspection.source
6055
+ })
6056
+ ];
6057
+ if (spec.expectedVersion) {
6058
+ const actualVersion = extractVersion(inspection.version ?? "");
6059
+ checks.push(makeCheck2({
6060
+ id: `command:${commandId(spec.command)}:version`,
6061
+ kind: "command",
6062
+ status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
6063
+ target: spec.command,
6064
+ expected: spec.expectedVersion,
6065
+ actual: actualVersion ?? inspection.version ?? "missing",
6066
+ detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
6067
+ source: inspection.source
6068
+ }));
6069
+ }
6070
+ return checks;
6071
+ }
6072
+ function packageCheck(machineId, spec, runner) {
6073
+ const command = spec.command ?? packageCommand(spec.name);
6074
+ const inspection = inspectCommand(machineId, { command, expectedVersion: spec.expectedVersion, required: spec.required }, runner);
6075
+ const found = Boolean(inspection.path);
6076
+ const checks = [
6077
+ makeCheck2({
6078
+ id: `package:${commandId(spec.name)}:command`,
6079
+ kind: "package",
6080
+ status: statusFor(spec.required, found),
6081
+ target: spec.name,
6082
+ expected: command,
6083
+ actual: inspection.path ?? "missing",
6084
+ detail: found ? `${command} found at ${inspection.path}` : `${command} command missing`,
6085
+ source: inspection.source
6086
+ })
6087
+ ];
6088
+ if (spec.expectedVersion) {
6089
+ const actualVersion = extractVersion(inspection.version ?? "");
6090
+ checks.push(makeCheck2({
6091
+ id: `package:${commandId(spec.name)}:version`,
6092
+ kind: "package",
6093
+ status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
6094
+ target: spec.name,
6095
+ expected: spec.expectedVersion,
6096
+ actual: actualVersion ?? inspection.version ?? "missing",
6097
+ detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
6098
+ source: inspection.source
6099
+ }));
6100
+ }
6101
+ return checks;
6102
+ }
6103
+ function workspaceCheck(machineId, spec, runner) {
6104
+ const inspection = inspectWorkspace(machineId, spec, runner);
6105
+ const target = spec.label ?? spec.path;
6106
+ const checks = [
6107
+ makeCheck2({
6108
+ id: `workspace:${commandId(target)}:path`,
6109
+ kind: "workspace",
6110
+ status: statusFor(spec.required, inspection.exists),
6111
+ target,
6112
+ expected: spec.path,
6113
+ actual: inspection.exists ? "exists" : "missing",
6114
+ detail: inspection.exists ? `workspace exists at ${spec.path}` : inspection.stderr || `workspace missing at ${spec.path}`,
6115
+ source: inspection.source
6116
+ })
6117
+ ];
6118
+ if (spec.expectedPackageName) {
6119
+ checks.push(makeCheck2({
6120
+ id: `workspace:${commandId(target)}:package-name`,
6121
+ kind: "workspace",
6122
+ status: inspection.packageName === spec.expectedPackageName ? "ok" : statusFor(spec.required, false),
6123
+ target,
6124
+ expected: spec.expectedPackageName,
6125
+ actual: inspection.packageName ?? (inspection.packageJson ? "missing-name" : "missing-package-json"),
6126
+ detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
6127
+ source: inspection.source
6128
+ }));
6129
+ }
6130
+ if (spec.expectedVersion) {
6131
+ checks.push(makeCheck2({
6132
+ id: `workspace:${commandId(target)}:version`,
6133
+ kind: "workspace",
6134
+ status: inspection.version === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
6135
+ target,
6136
+ expected: spec.expectedVersion,
6137
+ actual: inspection.version ?? (inspection.packageJson ? "missing-version" : "missing-package-json"),
6138
+ detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
6139
+ source: inspection.source
6140
+ }));
6141
+ }
6142
+ return checks;
6143
+ }
6144
+ function checkMachineCompatibility(options = {}) {
6145
+ const machineId = options.machineId ?? getLocalMachineId();
6146
+ const runner = options.runner ?? defaultRunner2;
6147
+ const commands = options.commands ?? DEFAULT_COMMANDS;
6148
+ const packages = options.packages ?? defaultPackages();
6149
+ const workspaces = options.workspaces ?? [];
6150
+ const checks = [];
6151
+ for (const spec of commands)
6152
+ checks.push(...commandCheck(machineId, spec, runner));
6153
+ for (const spec of packages)
6154
+ checks.push(...packageCheck(machineId, spec, runner));
6155
+ for (const spec of workspaces)
6156
+ checks.push(...workspaceCheck(machineId, spec, runner));
6157
+ const summary = {
6158
+ ok: checks.filter((check2) => check2.status === "ok").length,
6159
+ warn: checks.filter((check2) => check2.status === "warn").length,
6160
+ fail: checks.filter((check2) => check2.status === "fail").length
6161
+ };
6162
+ return {
6163
+ ok: summary.fail === 0,
6164
+ machine_id: machineId,
6165
+ source: checks[0]?.source ?? "local",
6166
+ generated_at: (options.now ?? new Date).toISOString(),
6167
+ checks,
6168
+ summary
6169
+ };
6170
+ }
6171
+
6172
+ // src/pg-migrations.ts
6173
+ var PG_MIGRATIONS = [
6174
+ `
6175
+ CREATE TABLE IF NOT EXISTS agent_heartbeats (
6176
+ machine_id TEXT NOT NULL,
6177
+ pid INTEGER NOT NULL,
6178
+ status TEXT NOT NULL,
6179
+ updated_at TIMESTAMPTZ NOT NULL,
6180
+ PRIMARY KEY (machine_id, pid)
6181
+ );
6182
+
6183
+ CREATE TABLE IF NOT EXISTS setup_runs (
6184
+ id TEXT PRIMARY KEY,
6185
+ machine_id TEXT NOT NULL,
6186
+ status TEXT NOT NULL,
6187
+ details_json TEXT NOT NULL DEFAULT '[]',
6188
+ created_at TIMESTAMPTZ NOT NULL,
6189
+ updated_at TIMESTAMPTZ NOT NULL
6190
+ );
6191
+
6192
+ CREATE TABLE IF NOT EXISTS sync_runs (
6193
+ id TEXT PRIMARY KEY,
6194
+ machine_id TEXT NOT NULL,
6195
+ status TEXT NOT NULL,
6196
+ actions_json TEXT NOT NULL DEFAULT '[]',
6197
+ created_at TIMESTAMPTZ NOT NULL,
6198
+ updated_at TIMESTAMPTZ NOT NULL
6199
+ );
6200
+ `
6201
+ ];
6202
+
6203
+ // src/remote-storage.ts
6204
+ import pg from "pg";
6205
+ function translatePlaceholders(sql) {
6206
+ let index = 0;
6207
+ return sql.replace(/\?/g, () => `$${++index}`);
6208
+ }
6209
+ function normalizeParams(params) {
6210
+ const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
6211
+ return flat.map((value) => value === undefined ? null : value);
6212
+ }
6213
+ function sslConfigFor(connectionString) {
6214
+ return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
6215
+ }
6216
+
6217
+ class PgAdapterAsync {
6218
+ pool;
6219
+ constructor(connectionString) {
6220
+ this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
6221
+ }
6222
+ async run(sql, ...params) {
6223
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
6224
+ return { changes: result.rowCount ?? 0 };
6225
+ }
6226
+ async all(sql, ...params) {
6227
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
6228
+ return result.rows;
6229
+ }
6230
+ async close() {
6231
+ await this.pool.end();
6232
+ }
6233
+ }
6234
+
6235
+ // src/storage-sync.ts
6236
+ var STORAGE_TABLES = [
6237
+ "agent_heartbeats",
6238
+ "setup_runs",
6239
+ "sync_runs"
6240
+ ];
6241
+ var MACHINES_STORAGE_ENV = "HASNA_MACHINES_DATABASE_URL";
6242
+ var MACHINES_STORAGE_FALLBACK_ENV = "MACHINES_DATABASE_URL";
6243
+ var MACHINES_STORAGE_MODE_ENV = "HASNA_MACHINES_STORAGE_MODE";
6244
+ var MACHINES_STORAGE_MODE_FALLBACK_ENV = "MACHINES_STORAGE_MODE";
6245
+ var STORAGE_DATABASE_ENV = [MACHINES_STORAGE_ENV, MACHINES_STORAGE_FALLBACK_ENV];
6246
+ var PRIMARY_KEYS = {
6247
+ agent_heartbeats: ["machine_id", "pid"],
6248
+ setup_runs: ["id"],
6249
+ sync_runs: ["id"]
6250
+ };
6251
+ function readEnv(name) {
6252
+ const value = process.env[name]?.trim();
6253
+ return value || undefined;
6254
+ }
6255
+ function normalizeStorageMode(value) {
6256
+ const normalized = value?.trim().toLowerCase();
6257
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
6258
+ return normalized;
6259
+ return;
6260
+ }
6261
+ function getStorageDatabaseEnvName() {
6262
+ for (const name of STORAGE_DATABASE_ENV) {
6263
+ if (readEnv(name))
6264
+ return name;
6265
+ }
6266
+ return null;
6267
+ }
6268
+ function getStorageDatabaseEnv() {
6269
+ const name = getStorageDatabaseEnvName();
6270
+ return name ? { name } : null;
6271
+ }
6272
+ function getStorageDatabaseUrl() {
6273
+ const env = getStorageDatabaseEnv();
6274
+ return env ? readEnv(env.name) ?? null : null;
6275
+ }
6276
+ function getStorageMode() {
6277
+ const mode = normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_FALLBACK_ENV));
6278
+ if (mode)
6279
+ return mode;
6280
+ return getStorageDatabaseUrl() ? "hybrid" : "local";
6281
+ }
6282
+ async function getStoragePg() {
6283
+ const url = getStorageDatabaseUrl();
6284
+ if (!url) {
6285
+ throw new Error("Missing HASNA_MACHINES_DATABASE_URL or MACHINES_DATABASE_URL");
6286
+ }
6287
+ return new PgAdapterAsync(url);
6288
+ }
6289
+ async function runStorageMigrations(remote) {
6290
+ for (const sql of PG_MIGRATIONS)
6291
+ await remote.run(sql);
6292
+ }
6293
+ async function storagePush(options) {
6294
+ const remote = await getStoragePg();
6295
+ const db = getDb();
6296
+ try {
6297
+ await runStorageMigrations(remote);
6298
+ const results = [];
6299
+ for (const table of resolveTables(options?.tables)) {
6300
+ results.push(await pushTable(db, remote, table));
6301
+ }
6302
+ recordSyncMeta(db, "push", results);
6303
+ return results;
6304
+ } finally {
6305
+ await remote.close();
6306
+ }
6307
+ }
6308
+ async function storagePull(options) {
6309
+ const remote = await getStoragePg();
6310
+ const db = getDb();
6311
+ try {
6312
+ await runStorageMigrations(remote);
6313
+ const results = [];
6314
+ for (const table of resolveTables(options?.tables)) {
6315
+ results.push(await pullTable(remote, db, table));
6316
+ }
6317
+ recordSyncMeta(db, "pull", results);
6318
+ return results;
6319
+ } finally {
6320
+ await remote.close();
6321
+ }
6322
+ }
6323
+ async function storageSync(options) {
6324
+ const pull = await storagePull(options);
6325
+ const push = await storagePush(options);
6326
+ return { pull, push };
6327
+ }
6328
+ function getSyncMetaAll() {
6329
+ const db = getDb();
6330
+ ensureSyncMetaTable(db);
6331
+ return db.query("SELECT table_name, last_synced_at, direction FROM _machines_sync_meta ORDER BY table_name, direction").all();
6332
+ }
6333
+ function getStorageStatus() {
6334
+ const activeEnv = getStorageDatabaseEnv();
6335
+ return {
6336
+ configured: Boolean(activeEnv),
6337
+ mode: getStorageMode(),
6338
+ env: STORAGE_DATABASE_ENV,
6339
+ activeEnv: activeEnv?.name ?? null,
6340
+ service: "machines",
6341
+ tables: STORAGE_TABLES,
6342
+ sync: getSyncMetaAll()
6343
+ };
6344
+ }
6345
+ function resolveTables(tables) {
6346
+ if (!tables || tables.length === 0)
6347
+ return [...STORAGE_TABLES];
6348
+ const allowed = new Set(STORAGE_TABLES);
6349
+ const requested = tables.map((table) => table.trim()).filter(Boolean);
6350
+ const invalid = requested.filter((table) => !allowed.has(table));
6351
+ if (invalid.length > 0)
6352
+ throw new Error(`Unknown machines storage table(s): ${invalid.join(", ")}`);
6353
+ return requested;
6354
+ }
6355
+ async function pushTable(db, remote, table) {
6356
+ const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
6357
+ try {
6358
+ if (!tableExists(db, table))
6359
+ return result;
6360
+ const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
6361
+ result.rowsRead = rows.length;
6362
+ if (rows.length === 0)
6363
+ return result;
6364
+ const remoteColumns = await getRemoteColumns(remote, table);
6365
+ const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
6366
+ result.rowsWritten = await upsertPg(remote, table, columns, rows);
6367
+ } catch (error) {
6368
+ result.errors.push(error instanceof Error ? error.message : String(error));
6369
+ }
6370
+ return result;
6371
+ }
6372
+ async function pullTable(remote, db, table) {
6373
+ const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
6374
+ try {
6375
+ if (!tableExists(db, table))
6376
+ return result;
6377
+ const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
6378
+ result.rowsRead = rows.length;
6379
+ if (rows.length === 0)
6380
+ return result;
6381
+ const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
6382
+ result.rowsWritten = upsertSqlite(db, table, columns, rows);
6383
+ } catch (error) {
6384
+ result.errors.push(error instanceof Error ? error.message : String(error));
6385
+ }
6386
+ return result;
6387
+ }
6388
+ async function getRemoteColumns(remote, table) {
6389
+ const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
6390
+ return new Set(rows.map((row) => row.column_name));
6391
+ }
6392
+ function filterRemoteColumns(remoteColumns, columns) {
6393
+ if (remoteColumns.size === 0)
6394
+ return columns;
6395
+ return columns.filter((column) => remoteColumns.has(column));
6396
+ }
6397
+ function filterLocalColumns(db, table, columns) {
6398
+ const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
6399
+ const allowed = new Set(rows.map((row) => row.name));
6400
+ return columns.filter((column) => allowed.has(column));
6401
+ }
6402
+ async function upsertPg(remote, table, columns, rows) {
6403
+ if (columns.length === 0)
6404
+ return 0;
6405
+ const primaryKeys = PRIMARY_KEYS[table];
6406
+ const columnList = columns.map(quoteIdent).join(", ");
6407
+ const placeholders = columns.map(() => "?").join(", ");
6408
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
6409
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
6410
+ const fallbackKey = primaryKeys[0];
6411
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
6412
+ for (const row of rows) {
6413
+ await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
6414
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => coerceForPg(row[column])));
6415
+ }
6416
+ return rows.length;
6417
+ }
6418
+ function upsertSqlite(db, table, columns, rows) {
6419
+ if (columns.length === 0)
6420
+ return 0;
6421
+ const primaryKeys = PRIMARY_KEYS[table];
6422
+ const columnList = columns.map(quoteIdent).join(", ");
6423
+ const placeholders = columns.map(() => "?").join(", ");
6424
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
6425
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
6426
+ const fallbackKey = primaryKeys[0];
6427
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
6428
+ const statement = db.query(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
6429
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
6430
+ const insert = db.transaction((batch) => {
6431
+ for (const row of batch)
6432
+ statement.run(...columns.map((column) => coerceForSqlite(row[column])));
6433
+ });
6434
+ insert(rows);
6435
+ return rows.length;
6436
+ }
6437
+ function recordSyncMeta(db, direction, results) {
6438
+ ensureSyncMetaTable(db);
6439
+ const now = new Date().toISOString();
6440
+ const statement = db.query(`
6441
+ INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
6442
+ VALUES (?, ?, ?)
6443
+ ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at
6444
+ `);
6445
+ for (const result of results) {
6446
+ if (result.errors.length > 0)
6447
+ continue;
6448
+ statement.run(result.table, now, direction);
6449
+ }
6450
+ }
6451
+ function ensureSyncMetaTable(db) {
6452
+ db.exec(`
6453
+ CREATE TABLE IF NOT EXISTS _machines_sync_meta (
6454
+ table_name TEXT NOT NULL,
6455
+ last_synced_at TEXT,
6456
+ direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')),
6457
+ PRIMARY KEY (table_name, direction)
6458
+ )
6459
+ `);
6460
+ }
6461
+ function tableExists(db, table) {
6462
+ const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
6463
+ return Boolean(row);
6464
+ }
6465
+ function quoteIdent(identifier) {
6466
+ return `"${identifier.replace(/"/g, '""')}"`;
6467
+ }
6468
+ function coerceForPg(value) {
6469
+ if (value === undefined || value === null)
6470
+ return null;
6471
+ if (value instanceof Date)
6472
+ return value.toISOString();
6473
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
6474
+ return value;
6475
+ if (typeof value === "object")
6476
+ return JSON.stringify(value);
6477
+ return value;
6478
+ }
6479
+ function coerceForSqlite(value) {
6480
+ if (value === undefined || value === null)
6481
+ return null;
6482
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
6483
+ return value;
6484
+ if (value instanceof Date)
6485
+ return value.toISOString();
6486
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
6487
+ return value;
6488
+ if (typeof value === "object")
6489
+ return JSON.stringify(value);
6490
+ return String(value);
6491
+ }
5759
6492
  // src/mcp/server.ts
6493
+ function buildServer(version = getPackageVersion()) {
6494
+ return createMcpServer(version);
6495
+ }
5760
6496
  function createMcpServer(version) {
5761
6497
  const server = new McpServer({ name: "machines", version });
5762
6498
  server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
@@ -5789,6 +6525,33 @@ function createMcpServer(version) {
5789
6525
  server.tool("machines_setup_apply", "Execute setup actions 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(runSetup(machine_id, { apply: true, yes }), null, 2) }] }));
5790
6526
  server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }] }));
5791
6527
  server.tool("machines_sync_apply", "Execute sync actions 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(runSync(machine_id, { apply: true, yes }), null, 2) }] }));
6528
+ server.tool("machines_topology", "Discover local, manifest, heartbeat, SSH, and Tailscale machine topology.", { include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json") }, async ({ include_tailscale }) => ({
6529
+ content: [{ type: "text", text: JSON.stringify(discoverMachineTopology({ includeTailscale: include_tailscale !== false }), null, 2) }]
6530
+ }));
6531
+ server.tool("machines_compatibility", "Check remote package, command, and workspace compatibility for open-* consumers.", {
6532
+ machine_id: exports_external.string().optional().describe("Machine identifier"),
6533
+ commands: exports_external.array(exports_external.object({
6534
+ command: exports_external.string(),
6535
+ expectedVersion: exports_external.string().optional(),
6536
+ versionArgs: exports_external.string().optional(),
6537
+ required: exports_external.boolean().optional()
6538
+ })).optional().describe("Commands to check"),
6539
+ packages: exports_external.array(exports_external.object({
6540
+ name: exports_external.string(),
6541
+ command: exports_external.string().optional(),
6542
+ expectedVersion: exports_external.string().optional(),
6543
+ required: exports_external.boolean().optional()
6544
+ })).optional().describe("Package-backed CLI checks"),
6545
+ workspaces: exports_external.array(exports_external.object({
6546
+ path: exports_external.string(),
6547
+ label: exports_external.string().optional(),
6548
+ expectedPackageName: exports_external.string().optional(),
6549
+ expectedVersion: exports_external.string().optional(),
6550
+ required: exports_external.boolean().optional()
6551
+ })).optional().describe("Workspace paths and package metadata to check")
6552
+ }, async ({ machine_id, commands, packages, workspaces }) => ({
6553
+ content: [{ type: "text", text: JSON.stringify(checkMachineCompatibility({ machineId: machine_id, commands, packages, workspaces }), null, 2) }]
6554
+ }));
5792
6555
  server.tool("machines_diff", "Show manifest differences between two machines.", {
5793
6556
  left_machine_id: exports_external.string().describe("Left machine identifier"),
5794
6557
  right_machine_id: exports_external.string().optional().describe("Right machine identifier")
@@ -5850,24 +6613,116 @@ function createMcpServer(version) {
5850
6613
  server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
5851
6614
  content: [{ type: "text", text: renderDashboardHtml() }]
5852
6615
  }));
6616
+ server.tool("storage_status", "Show machines storage sync configuration and local sync history.", {}, async () => ({
6617
+ content: [{ type: "text", text: JSON.stringify(getStorageStatus(), null, 2) }]
6618
+ }));
6619
+ server.tool("storage_push", "Push local machine runtime data to storage PostgreSQL.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to push") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storagePush(tables ? { tables } : undefined), null, 2) }] }));
6620
+ server.tool("storage_pull", "Pull machine runtime data from storage PostgreSQL to local SQLite.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to pull") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storagePull(tables ? { tables } : undefined), null, 2) }] }));
6621
+ server.tool("storage_sync", "Bidirectional machines storage sync: pull then push.", { tables: exports_external.array(exports_external.string()).optional().describe("Optional table list to sync") }, async ({ tables }) => ({ content: [{ type: "text", text: JSON.stringify(await storageSync(tables ? { tables } : undefined), null, 2) }] }));
5853
6622
  return server;
5854
6623
  }
5855
6624
 
5856
- // src/mcp/index.ts
5857
- function getPkgVersion() {
6625
+ // src/mcp/http.ts
6626
+ var DEFAULT_HTTP_PORT = 8821;
6627
+ var HTTP_NAME = "machines";
6628
+ function isHttpMode(args = process.argv.slice(2)) {
6629
+ return args.includes("--http") || process.env.MCP_HTTP === "1";
6630
+ }
6631
+ function resolveHttpPort(args = process.argv.slice(2)) {
6632
+ for (let i = 0;i < args.length; i++) {
6633
+ const arg = args[i];
6634
+ if (arg === "--port" && args[i + 1]) {
6635
+ return parsePort(args[i + 1]);
6636
+ }
6637
+ if (arg.startsWith("--port=")) {
6638
+ return parsePort(arg.slice("--port=".length));
6639
+ }
6640
+ }
6641
+ const envPort = process.env.MCP_HTTP_PORT;
6642
+ if (envPort) {
6643
+ return parsePort(envPort);
6644
+ }
6645
+ return DEFAULT_HTTP_PORT;
6646
+ }
6647
+ function parsePort(raw) {
6648
+ const port = Number.parseInt(raw, 10);
6649
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
6650
+ throw new Error(`Invalid port: ${raw}`);
6651
+ }
6652
+ return port;
6653
+ }
6654
+ function pathnameFromRequest(req) {
6655
+ return new URL(req.url ?? "/", "http://127.0.0.1").pathname;
6656
+ }
6657
+ async function readRequestBody(req) {
6658
+ if (req.method !== "POST" && req.method !== "DELETE") {
6659
+ return;
6660
+ }
6661
+ const chunks = [];
6662
+ for await (const chunk of req) {
6663
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
6664
+ }
6665
+ const text = Buffer.concat(chunks).toString("utf8");
6666
+ if (!text) {
6667
+ return;
6668
+ }
6669
+ return JSON.parse(text);
6670
+ }
6671
+ async function handleMcpRequest(req, res) {
6672
+ const server = buildServer();
6673
+ const transport = new StreamableHTTPServerTransport({
6674
+ sessionIdGenerator: undefined
6675
+ });
6676
+ await server.connect(transport);
5858
6677
  try {
5859
- const pkgPath = join6(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
5860
- return JSON.parse(readFileSync6(pkgPath, "utf8")).version || "0.0.0";
5861
- } catch {
5862
- return "0.0.0";
6678
+ const body = await readRequestBody(req);
6679
+ await transport.handleRequest(req, res, body);
6680
+ } finally {
6681
+ res.on("close", () => {
6682
+ transport.close().catch(() => {
6683
+ return;
6684
+ });
6685
+ server.close().catch(() => {
6686
+ return;
6687
+ });
6688
+ });
5863
6689
  }
5864
6690
  }
6691
+ function startHttpServer(options = {}) {
6692
+ const host = options.host ?? "127.0.0.1";
6693
+ const port = options.port ?? resolveHttpPort();
6694
+ const name = options.name ?? HTTP_NAME;
6695
+ const httpServer = createServer(async (req, res) => {
6696
+ const path = pathnameFromRequest(req);
6697
+ if (req.method === "GET" && path === "/health") {
6698
+ res.writeHead(200, { "content-type": "application/json" });
6699
+ res.end(JSON.stringify({ status: "ok", name }));
6700
+ return;
6701
+ }
6702
+ if (path === "/mcp") {
6703
+ await handleMcpRequest(req, res);
6704
+ return;
6705
+ }
6706
+ res.writeHead(404, { "content-type": "application/json" });
6707
+ res.end(JSON.stringify({ error: "Not found" }));
6708
+ });
6709
+ httpServer.listen(port, host, () => {
6710
+ const address = httpServer.address();
6711
+ const boundPort = typeof address === "object" && address ? address.port : port;
6712
+ console.error(`machines-mcp HTTP listening on http://${host}:${boundPort}`);
6713
+ });
6714
+ return httpServer;
6715
+ }
6716
+
6717
+ // src/mcp/index.ts
5865
6718
  function printHelp() {
5866
6719
  console.log(`Usage: machines-mcp [options]
5867
6720
 
5868
- MCP server for machine fleet management tools (stdio transport)
6721
+ MCP server for machine fleet management tools (stdio transport by default)
5869
6722
 
5870
6723
  Options:
6724
+ --http Start Streamable HTTP transport on 127.0.0.1 (or MCP_HTTP=1)
6725
+ --port <n> HTTP port (default: 8821, or MCP_HTTP_PORT env)
5871
6726
  -V, --version output the version number
5872
6727
  -h, --help display help for command`);
5873
6728
  }
@@ -5877,9 +6732,13 @@ if (args.includes("--help") || args.includes("-h")) {
5877
6732
  process.exit(0);
5878
6733
  }
5879
6734
  if (args.includes("--version") || args.includes("-V")) {
5880
- console.log(getPkgVersion());
6735
+ console.log(getPackageVersion());
5881
6736
  process.exit(0);
5882
6737
  }
5883
- var server = createMcpServer(getPkgVersion());
5884
- var transport = new StdioServerTransport;
5885
- await server.connect(transport);
6738
+ if (isHttpMode(args)) {
6739
+ startHttpServer({ port: resolveHttpPort(args) });
6740
+ } else {
6741
+ const server = buildServer();
6742
+ const transport = new StdioServerTransport;
6743
+ await server.connect(transport);
6744
+ }