@hasna/machines 0.0.14 → 0.0.16

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(),
@@ -4199,6 +4218,7 @@ function detectCurrentMachineManifest() {
4199
4218
 
4200
4219
  // src/remote.ts
4201
4220
  import { spawnSync as spawnSync2 } from "child_process";
4221
+ import { hostname as hostname3 } from "os";
4202
4222
 
4203
4223
  // src/db.ts
4204
4224
  import { Database } from "bun:sqlite";
@@ -4343,18 +4363,37 @@ function buildSshCommand(machineId, remoteCommand) {
4343
4363
  }
4344
4364
 
4345
4365
  // src/remote.ts
4366
+ function shellQuote(value) {
4367
+ return `'${value.replace(/'/g, "'\\''")}'`;
4368
+ }
4369
+ function machineIsLocal(machineId, localMachineId) {
4370
+ return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname3();
4371
+ }
4372
+ function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
4373
+ if (machineIsLocal(machineId, localMachineId)) {
4374
+ return { source: "local", shellCommand: command };
4375
+ }
4376
+ try {
4377
+ return {
4378
+ source: resolveSshTarget(machineId).route,
4379
+ shellCommand: buildSshCommand(machineId, command)
4380
+ };
4381
+ } catch (error) {
4382
+ if (String(error.message ?? error).includes("Machine not found in manifest")) {
4383
+ return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
4384
+ }
4385
+ throw error;
4386
+ }
4387
+ }
4346
4388
  function runMachineCommand(machineId, command) {
4347
- const localMachineId = getLocalMachineId();
4348
- const isLocal = machineId === localMachineId;
4349
- const route = isLocal ? "local" : resolveSshTarget(machineId).route;
4350
- const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
4351
- const result = spawnSync2("bash", ["-lc", shellCommand], {
4389
+ const resolved = resolveMachineCommand(machineId, command);
4390
+ const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
4352
4391
  encoding: "utf8",
4353
4392
  env: process.env
4354
4393
  });
4355
4394
  return {
4356
4395
  machineId,
4357
- source: route,
4396
+ source: resolved.source,
4358
4397
  stdout: result.stdout || "",
4359
4398
  stderr: result.stderr || "",
4360
4399
  exitCode: result.status ?? 1
@@ -4374,7 +4413,7 @@ function getAppManager(machine, app) {
4374
4413
  return "winget";
4375
4414
  return "apt";
4376
4415
  }
4377
- function shellQuote(value) {
4416
+ function shellQuote2(value) {
4378
4417
  return `'${value.replace(/'/g, `'\\''`)}'`;
4379
4418
  }
4380
4419
  function buildAppCommand(machine, app) {
@@ -4395,7 +4434,7 @@ function buildAppCommand(machine, app) {
4395
4434
  return `sudo apt-get install -y ${packageName}`;
4396
4435
  }
4397
4436
  function buildAppProbeCommand(machine, app) {
4398
- const packageName = shellQuote(getPackageName(app));
4437
+ const packageName = shellQuote2(getPackageName(app));
4399
4438
  const manager = getAppManager(machine, app);
4400
4439
  if (manager === "custom") {
4401
4440
  return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
@@ -4501,20 +4540,20 @@ function runAppsInstall(machineId, options = {}) {
4501
4540
 
4502
4541
  // src/commands/cert.ts
4503
4542
  import { homedir as homedir3, platform as platform2 } from "os";
4504
- import { join as join3 } from "path";
4543
+ import { join as join4 } from "path";
4505
4544
  function quote2(value) {
4506
4545
  return `'${value.replace(/'/g, `'\\''`)}'`;
4507
4546
  }
4508
4547
  function certDir() {
4509
- return join3(homedir3(), ".hasna", "machines", "certs");
4548
+ return join4(homedir3(), ".hasna", "machines", "certs");
4510
4549
  }
4511
4550
  function buildCertPlan(domains) {
4512
4551
  if (domains.length === 0) {
4513
4552
  throw new Error("At least one domain is required.");
4514
4553
  }
4515
4554
  const primary = domains[0];
4516
- const certPath = join3(certDir(), `${primary}.pem`);
4517
- const keyPath = join3(certDir(), `${primary}-key.pem`);
4555
+ const certPath = join4(certDir(), `${primary}.pem`);
4556
+ const keyPath = join4(certDir(), `${primary}-key.pem`);
4518
4557
  const steps = [];
4519
4558
  if (platform2() === "darwin") {
4520
4559
  steps.push({
@@ -4578,16 +4617,16 @@ function runCertPlan(domains, options = {}) {
4578
4617
  }
4579
4618
 
4580
4619
  // src/commands/dns.ts
4581
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4582
- import { join as join4 } from "path";
4620
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4621
+ import { join as join5 } from "path";
4583
4622
  function getDnsPath() {
4584
- return join4(getDataDir(), "dns.json");
4623
+ return join5(getDataDir(), "dns.json");
4585
4624
  }
4586
4625
  function readMappings() {
4587
4626
  const path = getDnsPath();
4588
- if (!existsSync3(path))
4627
+ if (!existsSync4(path))
4589
4628
  return [];
4590
- return JSON.parse(readFileSync2(path, "utf8"));
4629
+ return JSON.parse(readFileSync3(path, "utf8"));
4591
4630
  }
4592
4631
  function writeMappings(mappings) {
4593
4632
  const path = getDnsPath();
@@ -4614,10 +4653,10 @@ function renderDomainMapping(domain) {
4614
4653
  hostsEntry: `${entry.targetHost} ${entry.domain}`,
4615
4654
  caddySnippet: `${entry.domain} {
4616
4655
  reverse_proxy 127.0.0.1:${entry.port}
4617
- tls ${join4(getDataDir(), "certs", `${entry.domain}.pem`)} ${join4(getDataDir(), "certs", `${entry.domain}-key.pem`)}
4656
+ tls ${join5(getDataDir(), "certs", `${entry.domain}.pem`)} ${join5(getDataDir(), "certs", `${entry.domain}-key.pem`)}
4618
4657
  }`,
4619
- certPath: join4(getDataDir(), "certs", `${entry.domain}.pem`),
4620
- keyPath: join4(getDataDir(), "certs", `${entry.domain}-key.pem`)
4658
+ certPath: join5(getDataDir(), "certs", `${entry.domain}.pem`),
4659
+ keyPath: join5(getDataDir(), "certs", `${entry.domain}-key.pem`)
4621
4660
  };
4622
4661
  }
4623
4662
 
@@ -4889,7 +4928,7 @@ function runTailscaleInstall(machineId, options = {}) {
4889
4928
  }
4890
4929
 
4891
4930
  // src/commands/notifications.ts
4892
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4931
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4893
4932
  var notificationChannelSchema = exports_external.object({
4894
4933
  id: exports_external.string(),
4895
4934
  type: exports_external.enum(["email", "webhook", "command"]),
@@ -4905,7 +4944,7 @@ var notificationConfigSchema = exports_external.object({
4905
4944
  function sortChannels(channels) {
4906
4945
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
4907
4946
  }
4908
- function shellQuote2(value) {
4947
+ function shellQuote3(value) {
4909
4948
  return `'${value.replace(/'/g, `'\\''`)}'`;
4910
4949
  }
4911
4950
  function hasCommand(binary) {
@@ -4952,7 +4991,7 @@ ${message}
4952
4991
  };
4953
4992
  }
4954
4993
  if (hasCommand("mail")) {
4955
- const command = `printf %s ${shellQuote2(message)} | mail -s ${shellQuote2(subject)} ${shellQuote2(channel.target)}`;
4994
+ const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
4956
4995
  const result = Bun.spawnSync(["bash", "-lc", command], {
4957
4996
  stdout: "pipe",
4958
4997
  stderr: "pipe",
@@ -5045,10 +5084,10 @@ function getDefaultNotificationConfig() {
5045
5084
  };
5046
5085
  }
5047
5086
  function readNotificationConfig(path = getNotificationsPath()) {
5048
- if (!existsSync4(path)) {
5087
+ if (!existsSync5(path)) {
5049
5088
  return getDefaultNotificationConfig();
5050
5089
  }
5051
- return notificationConfigSchema.parse(JSON.parse(readFileSync3(path, "utf8")));
5090
+ return notificationConfigSchema.parse(JSON.parse(readFileSync4(path, "utf8")));
5052
5091
  }
5053
5092
  function writeNotificationConfig(config, path = getNotificationsPath()) {
5054
5093
  ensureParentDir(path);
@@ -5220,24 +5259,6 @@ function manifestValidate() {
5220
5259
  return validateManifest(getManifestPath());
5221
5260
  }
5222
5261
 
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
5262
  // src/commands/status.ts
5242
5263
  function getStatus() {
5243
5264
  const manifest = readManifest();
@@ -5756,7 +5777,742 @@ function getAgentStatus(machineId = getLocalMachineId()) {
5756
5777
  }));
5757
5778
  }
5758
5779
 
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
+ // src/compatibility.ts
5958
+ var DEFAULT_COMMANDS = [
5959
+ { command: "bun", required: true },
5960
+ { command: "machines", required: true }
5961
+ ];
5962
+ function defaultPackages() {
5963
+ return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
5964
+ }
5965
+ function shellQuote4(value) {
5966
+ return `'${value.replace(/'/g, "'\\''")}'`;
5967
+ }
5968
+ function commandId(value) {
5969
+ return value.replace(/[^a-zA-Z0-9_.@/-]+/g, "-").replace(/^-+|-+$/g, "");
5970
+ }
5971
+ function packageCommand(name) {
5972
+ if (name === "@hasna/knowledge")
5973
+ return "knowledge";
5974
+ if (name === "@hasna/machines")
5975
+ return "machines";
5976
+ return name.split("/").pop() ?? name;
5977
+ }
5978
+ function firstLine(value) {
5979
+ return value.trim().split(/\r?\n/).find(Boolean) ?? "";
5980
+ }
5981
+ function extractVersion(value) {
5982
+ const match = value.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
5983
+ return match?.[0] ?? null;
5984
+ }
5985
+ function statusFor(required, ok) {
5986
+ if (ok)
5987
+ return "ok";
5988
+ return required === false ? "warn" : "fail";
5989
+ }
5990
+ function makeCheck2(input) {
5991
+ return {
5992
+ id: input.id,
5993
+ kind: input.kind,
5994
+ status: input.status,
5995
+ target: input.target,
5996
+ expected: input.expected ?? null,
5997
+ actual: input.actual ?? null,
5998
+ detail: input.detail,
5999
+ source: input.source
6000
+ };
6001
+ }
6002
+ function parseKeyValue(stdout) {
6003
+ const result = {};
6004
+ for (const line of stdout.split(/\r?\n/)) {
6005
+ const idx = line.indexOf("=");
6006
+ if (idx <= 0)
6007
+ continue;
6008
+ result[line.slice(0, idx)] = line.slice(idx + 1);
6009
+ }
6010
+ return result;
6011
+ }
6012
+ function defaultRunner2(machineId, command) {
6013
+ return runMachineCommand(machineId, command);
6014
+ }
6015
+ function inspectCommand(machineId, spec, runner) {
6016
+ const command = shellQuote4(spec.command);
6017
+ const versionArgs = spec.versionArgs ?? "--version";
6018
+ const script = [
6019
+ `cmd=${command}`,
6020
+ 'path="$(command -v "$cmd" 2>/dev/null || true)"',
6021
+ 'printf "path=%s\\n" "$path"',
6022
+ 'if [ -n "$path" ]; then version="$("$cmd" ' + versionArgs + ' 2>/dev/null || true)"; printf "version=%s\\n" "$version"; fi'
6023
+ ].join("; ");
6024
+ const result = runner(machineId, script);
6025
+ const parsed = parseKeyValue(result.stdout);
6026
+ return {
6027
+ path: parsed.path || null,
6028
+ version: parsed.version ? firstLine(parsed.version) : null,
6029
+ exitCode: result.exitCode,
6030
+ source: result.source,
6031
+ stderr: result.stderr
6032
+ };
6033
+ }
6034
+ function fieldCommand(field) {
6035
+ const regex = field === "name" ? String.raw`s/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p` : String.raw`s/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p`;
6036
+ return [
6037
+ `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`,
6038
+ `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`,
6039
+ `else sed -n '${regex}' "$pkg" | head -n 1`,
6040
+ "fi"
6041
+ ].join("; ");
6042
+ }
6043
+ function inspectWorkspace(machineId, spec, runner) {
6044
+ const script = [
6045
+ `path=${shellQuote4(spec.path)}`,
6046
+ 'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
6047
+ 'pkg="$path/package.json"',
6048
+ 'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
6049
+ `if [ -f "$pkg" ]; then printf "package_name=%s\\n" "$(${fieldCommand("name")})"; printf "version=%s\\n" "$(${fieldCommand("version")})"; fi`
6050
+ ].join("; ");
6051
+ const result = runner(machineId, script);
6052
+ const parsed = parseKeyValue(result.stdout);
6053
+ return {
6054
+ exists: parsed.exists === "yes",
6055
+ packageJson: parsed.package_json === "yes",
6056
+ packageName: parsed.package_name || null,
6057
+ version: parsed.version || null,
6058
+ source: result.source,
6059
+ stderr: result.stderr
6060
+ };
6061
+ }
6062
+ function commandCheck(machineId, spec, runner) {
6063
+ const inspection = inspectCommand(machineId, spec, runner);
6064
+ const found = Boolean(inspection.path);
6065
+ const checks = [
6066
+ makeCheck2({
6067
+ id: `command:${commandId(spec.command)}:path`,
6068
+ kind: "command",
6069
+ status: statusFor(spec.required, found),
6070
+ target: spec.command,
6071
+ expected: "available",
6072
+ actual: inspection.path ?? "missing",
6073
+ detail: found ? `found at ${inspection.path}` : inspection.stderr || "command missing",
6074
+ source: inspection.source
6075
+ })
6076
+ ];
6077
+ if (spec.expectedVersion) {
6078
+ const actualVersion = extractVersion(inspection.version ?? "");
6079
+ checks.push(makeCheck2({
6080
+ id: `command:${commandId(spec.command)}:version`,
6081
+ kind: "command",
6082
+ status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
6083
+ target: spec.command,
6084
+ expected: spec.expectedVersion,
6085
+ actual: actualVersion ?? inspection.version ?? "missing",
6086
+ detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
6087
+ source: inspection.source
6088
+ }));
6089
+ }
6090
+ return checks;
6091
+ }
6092
+ function packageCheck(machineId, spec, runner) {
6093
+ const command = spec.command ?? packageCommand(spec.name);
6094
+ const inspection = inspectCommand(machineId, { command, expectedVersion: spec.expectedVersion, required: spec.required }, runner);
6095
+ const found = Boolean(inspection.path);
6096
+ const checks = [
6097
+ makeCheck2({
6098
+ id: `package:${commandId(spec.name)}:command`,
6099
+ kind: "package",
6100
+ status: statusFor(spec.required, found),
6101
+ target: spec.name,
6102
+ expected: command,
6103
+ actual: inspection.path ?? "missing",
6104
+ detail: found ? `${command} found at ${inspection.path}` : `${command} command missing`,
6105
+ source: inspection.source
6106
+ })
6107
+ ];
6108
+ if (spec.expectedVersion) {
6109
+ const actualVersion = extractVersion(inspection.version ?? "");
6110
+ checks.push(makeCheck2({
6111
+ id: `package:${commandId(spec.name)}:version`,
6112
+ kind: "package",
6113
+ status: actualVersion === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
6114
+ target: spec.name,
6115
+ expected: spec.expectedVersion,
6116
+ actual: actualVersion ?? inspection.version ?? "missing",
6117
+ detail: actualVersion ? `version output: ${inspection.version}` : "version unavailable",
6118
+ source: inspection.source
6119
+ }));
6120
+ }
6121
+ return checks;
6122
+ }
6123
+ function workspaceCheck(machineId, spec, runner) {
6124
+ const inspection = inspectWorkspace(machineId, spec, runner);
6125
+ const target = spec.label ?? spec.path;
6126
+ const checks = [
6127
+ makeCheck2({
6128
+ id: `workspace:${commandId(target)}:path`,
6129
+ kind: "workspace",
6130
+ status: statusFor(spec.required, inspection.exists),
6131
+ target,
6132
+ expected: spec.path,
6133
+ actual: inspection.exists ? "exists" : "missing",
6134
+ detail: inspection.exists ? `workspace exists at ${spec.path}` : inspection.stderr || `workspace missing at ${spec.path}`,
6135
+ source: inspection.source
6136
+ })
6137
+ ];
6138
+ if (spec.expectedPackageName) {
6139
+ checks.push(makeCheck2({
6140
+ id: `workspace:${commandId(target)}:package-name`,
6141
+ kind: "workspace",
6142
+ status: inspection.packageName === spec.expectedPackageName ? "ok" : statusFor(spec.required, false),
6143
+ target,
6144
+ expected: spec.expectedPackageName,
6145
+ actual: inspection.packageName ?? (inspection.packageJson ? "missing-name" : "missing-package-json"),
6146
+ detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
6147
+ source: inspection.source
6148
+ }));
6149
+ }
6150
+ if (spec.expectedVersion) {
6151
+ checks.push(makeCheck2({
6152
+ id: `workspace:${commandId(target)}:version`,
6153
+ kind: "workspace",
6154
+ status: inspection.version === spec.expectedVersion ? "ok" : statusFor(spec.required, false),
6155
+ target,
6156
+ expected: spec.expectedVersion,
6157
+ actual: inspection.version ?? (inspection.packageJson ? "missing-version" : "missing-package-json"),
6158
+ detail: inspection.packageJson ? "package.json inspected" : "package.json missing",
6159
+ source: inspection.source
6160
+ }));
6161
+ }
6162
+ return checks;
6163
+ }
6164
+ function checkMachineCompatibility(options = {}) {
6165
+ const machineId = options.machineId ?? getLocalMachineId();
6166
+ const runner = options.runner ?? defaultRunner2;
6167
+ const commands = options.commands ?? DEFAULT_COMMANDS;
6168
+ const packages = options.packages ?? defaultPackages();
6169
+ const workspaces = options.workspaces ?? [];
6170
+ const checks = [];
6171
+ for (const spec of commands)
6172
+ checks.push(...commandCheck(machineId, spec, runner));
6173
+ for (const spec of packages)
6174
+ checks.push(...packageCheck(machineId, spec, runner));
6175
+ for (const spec of workspaces)
6176
+ checks.push(...workspaceCheck(machineId, spec, runner));
6177
+ const summary = {
6178
+ ok: checks.filter((check2) => check2.status === "ok").length,
6179
+ warn: checks.filter((check2) => check2.status === "warn").length,
6180
+ fail: checks.filter((check2) => check2.status === "fail").length
6181
+ };
6182
+ return {
6183
+ ok: summary.fail === 0,
6184
+ machine_id: machineId,
6185
+ source: checks[0]?.source ?? "local",
6186
+ generated_at: (options.now ?? new Date).toISOString(),
6187
+ checks,
6188
+ summary
6189
+ };
6190
+ }
6191
+
6192
+ // src/pg-migrations.ts
6193
+ var PG_MIGRATIONS = [
6194
+ `
6195
+ CREATE TABLE IF NOT EXISTS agent_heartbeats (
6196
+ machine_id TEXT NOT NULL,
6197
+ pid INTEGER NOT NULL,
6198
+ status TEXT NOT NULL,
6199
+ updated_at TIMESTAMPTZ NOT NULL,
6200
+ PRIMARY KEY (machine_id, pid)
6201
+ );
6202
+
6203
+ CREATE TABLE IF NOT EXISTS setup_runs (
6204
+ id TEXT PRIMARY KEY,
6205
+ machine_id TEXT NOT NULL,
6206
+ status TEXT NOT NULL,
6207
+ details_json TEXT NOT NULL DEFAULT '[]',
6208
+ created_at TIMESTAMPTZ NOT NULL,
6209
+ updated_at TIMESTAMPTZ NOT NULL
6210
+ );
6211
+
6212
+ CREATE TABLE IF NOT EXISTS sync_runs (
6213
+ id TEXT PRIMARY KEY,
6214
+ machine_id TEXT NOT NULL,
6215
+ status TEXT NOT NULL,
6216
+ actions_json TEXT NOT NULL DEFAULT '[]',
6217
+ created_at TIMESTAMPTZ NOT NULL,
6218
+ updated_at TIMESTAMPTZ NOT NULL
6219
+ );
6220
+ `
6221
+ ];
6222
+
6223
+ // src/remote-storage.ts
6224
+ import pg from "pg";
6225
+ function translatePlaceholders(sql) {
6226
+ let index = 0;
6227
+ return sql.replace(/\?/g, () => `$${++index}`);
6228
+ }
6229
+ function normalizeParams(params) {
6230
+ const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
6231
+ return flat.map((value) => value === undefined ? null : value);
6232
+ }
6233
+ function sslConfigFor(connectionString) {
6234
+ return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
6235
+ }
6236
+
6237
+ class PgAdapterAsync {
6238
+ pool;
6239
+ constructor(connectionString) {
6240
+ this.pool = new pg.Pool({ connectionString, ssl: sslConfigFor(connectionString) });
6241
+ }
6242
+ async run(sql, ...params) {
6243
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
6244
+ return { changes: result.rowCount ?? 0 };
6245
+ }
6246
+ async all(sql, ...params) {
6247
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
6248
+ return result.rows;
6249
+ }
6250
+ async close() {
6251
+ await this.pool.end();
6252
+ }
6253
+ }
6254
+
6255
+ // src/storage-sync.ts
6256
+ var STORAGE_TABLES = [
6257
+ "agent_heartbeats",
6258
+ "setup_runs",
6259
+ "sync_runs"
6260
+ ];
6261
+ var MACHINES_STORAGE_ENV = "HASNA_MACHINES_DATABASE_URL";
6262
+ var MACHINES_STORAGE_FALLBACK_ENV = "MACHINES_DATABASE_URL";
6263
+ var MACHINES_STORAGE_MODE_ENV = "HASNA_MACHINES_STORAGE_MODE";
6264
+ var MACHINES_STORAGE_MODE_FALLBACK_ENV = "MACHINES_STORAGE_MODE";
6265
+ var STORAGE_DATABASE_ENV = [MACHINES_STORAGE_ENV, MACHINES_STORAGE_FALLBACK_ENV];
6266
+ var PRIMARY_KEYS = {
6267
+ agent_heartbeats: ["machine_id", "pid"],
6268
+ setup_runs: ["id"],
6269
+ sync_runs: ["id"]
6270
+ };
6271
+ function readEnv(name) {
6272
+ const value = process.env[name]?.trim();
6273
+ return value || undefined;
6274
+ }
6275
+ function normalizeStorageMode(value) {
6276
+ const normalized = value?.trim().toLowerCase();
6277
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
6278
+ return normalized;
6279
+ return;
6280
+ }
6281
+ function getStorageDatabaseEnvName() {
6282
+ for (const name of STORAGE_DATABASE_ENV) {
6283
+ if (readEnv(name))
6284
+ return name;
6285
+ }
6286
+ return null;
6287
+ }
6288
+ function getStorageDatabaseEnv() {
6289
+ const name = getStorageDatabaseEnvName();
6290
+ return name ? { name } : null;
6291
+ }
6292
+ function getStorageDatabaseUrl() {
6293
+ const env = getStorageDatabaseEnv();
6294
+ return env ? readEnv(env.name) ?? null : null;
6295
+ }
6296
+ function getStorageMode() {
6297
+ const mode = normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_FALLBACK_ENV));
6298
+ if (mode)
6299
+ return mode;
6300
+ return getStorageDatabaseUrl() ? "hybrid" : "local";
6301
+ }
6302
+ async function getStoragePg() {
6303
+ const url = getStorageDatabaseUrl();
6304
+ if (!url) {
6305
+ throw new Error("Missing HASNA_MACHINES_DATABASE_URL or MACHINES_DATABASE_URL");
6306
+ }
6307
+ return new PgAdapterAsync(url);
6308
+ }
6309
+ async function runStorageMigrations(remote) {
6310
+ for (const sql of PG_MIGRATIONS)
6311
+ await remote.run(sql);
6312
+ }
6313
+ async function storagePush(options) {
6314
+ const remote = await getStoragePg();
6315
+ const db = getDb();
6316
+ try {
6317
+ await runStorageMigrations(remote);
6318
+ const results = [];
6319
+ for (const table of resolveTables(options?.tables)) {
6320
+ results.push(await pushTable(db, remote, table));
6321
+ }
6322
+ recordSyncMeta(db, "push", results);
6323
+ return results;
6324
+ } finally {
6325
+ await remote.close();
6326
+ }
6327
+ }
6328
+ async function storagePull(options) {
6329
+ const remote = await getStoragePg();
6330
+ const db = getDb();
6331
+ try {
6332
+ await runStorageMigrations(remote);
6333
+ const results = [];
6334
+ for (const table of resolveTables(options?.tables)) {
6335
+ results.push(await pullTable(remote, db, table));
6336
+ }
6337
+ recordSyncMeta(db, "pull", results);
6338
+ return results;
6339
+ } finally {
6340
+ await remote.close();
6341
+ }
6342
+ }
6343
+ async function storageSync(options) {
6344
+ const pull = await storagePull(options);
6345
+ const push = await storagePush(options);
6346
+ return { pull, push };
6347
+ }
6348
+ function getSyncMetaAll() {
6349
+ const db = getDb();
6350
+ ensureSyncMetaTable(db);
6351
+ return db.query("SELECT table_name, last_synced_at, direction FROM _machines_sync_meta ORDER BY table_name, direction").all();
6352
+ }
6353
+ function getStorageStatus() {
6354
+ const activeEnv = getStorageDatabaseEnv();
6355
+ return {
6356
+ configured: Boolean(activeEnv),
6357
+ mode: getStorageMode(),
6358
+ env: STORAGE_DATABASE_ENV,
6359
+ activeEnv: activeEnv?.name ?? null,
6360
+ service: "machines",
6361
+ tables: STORAGE_TABLES,
6362
+ sync: getSyncMetaAll()
6363
+ };
6364
+ }
6365
+ function resolveTables(tables) {
6366
+ if (!tables || tables.length === 0)
6367
+ return [...STORAGE_TABLES];
6368
+ const allowed = new Set(STORAGE_TABLES);
6369
+ const requested = tables.map((table) => table.trim()).filter(Boolean);
6370
+ const invalid = requested.filter((table) => !allowed.has(table));
6371
+ if (invalid.length > 0)
6372
+ throw new Error(`Unknown machines storage table(s): ${invalid.join(", ")}`);
6373
+ return requested;
6374
+ }
6375
+ async function pushTable(db, remote, table) {
6376
+ const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
6377
+ try {
6378
+ if (!tableExists(db, table))
6379
+ return result;
6380
+ const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
6381
+ result.rowsRead = rows.length;
6382
+ if (rows.length === 0)
6383
+ return result;
6384
+ const remoteColumns = await getRemoteColumns(remote, table);
6385
+ const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
6386
+ result.rowsWritten = await upsertPg(remote, table, columns, rows);
6387
+ } catch (error) {
6388
+ result.errors.push(error instanceof Error ? error.message : String(error));
6389
+ }
6390
+ return result;
6391
+ }
6392
+ async function pullTable(remote, db, table) {
6393
+ const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
6394
+ try {
6395
+ if (!tableExists(db, table))
6396
+ return result;
6397
+ const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
6398
+ result.rowsRead = rows.length;
6399
+ if (rows.length === 0)
6400
+ return result;
6401
+ const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
6402
+ result.rowsWritten = upsertSqlite(db, table, columns, rows);
6403
+ } catch (error) {
6404
+ result.errors.push(error instanceof Error ? error.message : String(error));
6405
+ }
6406
+ return result;
6407
+ }
6408
+ async function getRemoteColumns(remote, table) {
6409
+ const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
6410
+ return new Set(rows.map((row) => row.column_name));
6411
+ }
6412
+ function filterRemoteColumns(remoteColumns, columns) {
6413
+ if (remoteColumns.size === 0)
6414
+ return columns;
6415
+ return columns.filter((column) => remoteColumns.has(column));
6416
+ }
6417
+ function filterLocalColumns(db, table, columns) {
6418
+ const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
6419
+ const allowed = new Set(rows.map((row) => row.name));
6420
+ return columns.filter((column) => allowed.has(column));
6421
+ }
6422
+ async function upsertPg(remote, table, columns, rows) {
6423
+ if (columns.length === 0)
6424
+ return 0;
6425
+ const primaryKeys = PRIMARY_KEYS[table];
6426
+ const columnList = columns.map(quoteIdent).join(", ");
6427
+ const placeholders = columns.map(() => "?").join(", ");
6428
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
6429
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
6430
+ const fallbackKey = primaryKeys[0];
6431
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
6432
+ for (const row of rows) {
6433
+ await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
6434
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => coerceForPg(row[column])));
6435
+ }
6436
+ return rows.length;
6437
+ }
6438
+ function upsertSqlite(db, table, columns, rows) {
6439
+ if (columns.length === 0)
6440
+ return 0;
6441
+ const primaryKeys = PRIMARY_KEYS[table];
6442
+ const columnList = columns.map(quoteIdent).join(", ");
6443
+ const placeholders = columns.map(() => "?").join(", ");
6444
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
6445
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
6446
+ const fallbackKey = primaryKeys[0];
6447
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
6448
+ const statement = db.query(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders})
6449
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
6450
+ const insert = db.transaction((batch) => {
6451
+ for (const row of batch)
6452
+ statement.run(...columns.map((column) => coerceForSqlite(row[column])));
6453
+ });
6454
+ insert(rows);
6455
+ return rows.length;
6456
+ }
6457
+ function recordSyncMeta(db, direction, results) {
6458
+ ensureSyncMetaTable(db);
6459
+ const now = new Date().toISOString();
6460
+ const statement = db.query(`
6461
+ INSERT INTO _machines_sync_meta (table_name, last_synced_at, direction)
6462
+ VALUES (?, ?, ?)
6463
+ ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at
6464
+ `);
6465
+ for (const result of results) {
6466
+ if (result.errors.length > 0)
6467
+ continue;
6468
+ statement.run(result.table, now, direction);
6469
+ }
6470
+ }
6471
+ function ensureSyncMetaTable(db) {
6472
+ db.exec(`
6473
+ CREATE TABLE IF NOT EXISTS _machines_sync_meta (
6474
+ table_name TEXT NOT NULL,
6475
+ last_synced_at TEXT,
6476
+ direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')),
6477
+ PRIMARY KEY (table_name, direction)
6478
+ )
6479
+ `);
6480
+ }
6481
+ function tableExists(db, table) {
6482
+ const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
6483
+ return Boolean(row);
6484
+ }
6485
+ function quoteIdent(identifier) {
6486
+ return `"${identifier.replace(/"/g, '""')}"`;
6487
+ }
6488
+ function coerceForPg(value) {
6489
+ if (value === undefined || value === null)
6490
+ return null;
6491
+ if (value instanceof Date)
6492
+ return value.toISOString();
6493
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
6494
+ return value;
6495
+ if (typeof value === "object")
6496
+ return JSON.stringify(value);
6497
+ return value;
6498
+ }
6499
+ function coerceForSqlite(value) {
6500
+ if (value === undefined || value === null)
6501
+ return null;
6502
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
6503
+ return value;
6504
+ if (value instanceof Date)
6505
+ return value.toISOString();
6506
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
6507
+ return value;
6508
+ if (typeof value === "object")
6509
+ return JSON.stringify(value);
6510
+ return String(value);
6511
+ }
5759
6512
  // src/mcp/server.ts
6513
+ function buildServer(version = getPackageVersion()) {
6514
+ return createMcpServer(version);
6515
+ }
5760
6516
  function createMcpServer(version) {
5761
6517
  const server = new McpServer({ name: "machines", version });
5762
6518
  server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
@@ -5789,6 +6545,33 @@ function createMcpServer(version) {
5789
6545
  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
6546
  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
6547
  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) }] }));
6548
+ 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 }) => ({
6549
+ content: [{ type: "text", text: JSON.stringify(discoverMachineTopology({ includeTailscale: include_tailscale !== false }), null, 2) }]
6550
+ }));
6551
+ server.tool("machines_compatibility", "Check remote package, command, and workspace compatibility for open-* consumers.", {
6552
+ machine_id: exports_external.string().optional().describe("Machine identifier"),
6553
+ commands: exports_external.array(exports_external.object({
6554
+ command: exports_external.string(),
6555
+ expectedVersion: exports_external.string().optional(),
6556
+ versionArgs: exports_external.string().optional(),
6557
+ required: exports_external.boolean().optional()
6558
+ })).optional().describe("Commands to check"),
6559
+ packages: exports_external.array(exports_external.object({
6560
+ name: exports_external.string(),
6561
+ command: exports_external.string().optional(),
6562
+ expectedVersion: exports_external.string().optional(),
6563
+ required: exports_external.boolean().optional()
6564
+ })).optional().describe("Package-backed CLI checks"),
6565
+ workspaces: exports_external.array(exports_external.object({
6566
+ path: exports_external.string(),
6567
+ label: exports_external.string().optional(),
6568
+ expectedPackageName: exports_external.string().optional(),
6569
+ expectedVersion: exports_external.string().optional(),
6570
+ required: exports_external.boolean().optional()
6571
+ })).optional().describe("Workspace paths and package metadata to check")
6572
+ }, async ({ machine_id, commands, packages, workspaces }) => ({
6573
+ content: [{ type: "text", text: JSON.stringify(checkMachineCompatibility({ machineId: machine_id, commands, packages, workspaces }), null, 2) }]
6574
+ }));
5792
6575
  server.tool("machines_diff", "Show manifest differences between two machines.", {
5793
6576
  left_machine_id: exports_external.string().describe("Left machine identifier"),
5794
6577
  right_machine_id: exports_external.string().optional().describe("Right machine identifier")
@@ -5850,24 +6633,116 @@ function createMcpServer(version) {
5850
6633
  server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
5851
6634
  content: [{ type: "text", text: renderDashboardHtml() }]
5852
6635
  }));
6636
+ server.tool("storage_status", "Show machines storage sync configuration and local sync history.", {}, async () => ({
6637
+ content: [{ type: "text", text: JSON.stringify(getStorageStatus(), null, 2) }]
6638
+ }));
6639
+ 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) }] }));
6640
+ 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) }] }));
6641
+ 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
6642
  return server;
5854
6643
  }
5855
6644
 
5856
- // src/mcp/index.ts
5857
- function getPkgVersion() {
6645
+ // src/mcp/http.ts
6646
+ var DEFAULT_HTTP_PORT = 8821;
6647
+ var HTTP_NAME = "machines";
6648
+ function isHttpMode(args = process.argv.slice(2)) {
6649
+ return args.includes("--http") || process.env.MCP_HTTP === "1";
6650
+ }
6651
+ function resolveHttpPort(args = process.argv.slice(2)) {
6652
+ for (let i = 0;i < args.length; i++) {
6653
+ const arg = args[i];
6654
+ if (arg === "--port" && args[i + 1]) {
6655
+ return parsePort(args[i + 1]);
6656
+ }
6657
+ if (arg.startsWith("--port=")) {
6658
+ return parsePort(arg.slice("--port=".length));
6659
+ }
6660
+ }
6661
+ const envPort = process.env.MCP_HTTP_PORT;
6662
+ if (envPort) {
6663
+ return parsePort(envPort);
6664
+ }
6665
+ return DEFAULT_HTTP_PORT;
6666
+ }
6667
+ function parsePort(raw) {
6668
+ const port = Number.parseInt(raw, 10);
6669
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
6670
+ throw new Error(`Invalid port: ${raw}`);
6671
+ }
6672
+ return port;
6673
+ }
6674
+ function pathnameFromRequest(req) {
6675
+ return new URL(req.url ?? "/", "http://127.0.0.1").pathname;
6676
+ }
6677
+ async function readRequestBody(req) {
6678
+ if (req.method !== "POST" && req.method !== "DELETE") {
6679
+ return;
6680
+ }
6681
+ const chunks = [];
6682
+ for await (const chunk of req) {
6683
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
6684
+ }
6685
+ const text = Buffer.concat(chunks).toString("utf8");
6686
+ if (!text) {
6687
+ return;
6688
+ }
6689
+ return JSON.parse(text);
6690
+ }
6691
+ async function handleMcpRequest(req, res) {
6692
+ const server = buildServer();
6693
+ const transport = new StreamableHTTPServerTransport({
6694
+ sessionIdGenerator: undefined
6695
+ });
6696
+ await server.connect(transport);
5858
6697
  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";
6698
+ const body = await readRequestBody(req);
6699
+ await transport.handleRequest(req, res, body);
6700
+ } finally {
6701
+ res.on("close", () => {
6702
+ transport.close().catch(() => {
6703
+ return;
6704
+ });
6705
+ server.close().catch(() => {
6706
+ return;
6707
+ });
6708
+ });
5863
6709
  }
5864
6710
  }
6711
+ function startHttpServer(options = {}) {
6712
+ const host = options.host ?? "127.0.0.1";
6713
+ const port = options.port ?? resolveHttpPort();
6714
+ const name = options.name ?? HTTP_NAME;
6715
+ const httpServer = createServer(async (req, res) => {
6716
+ const path = pathnameFromRequest(req);
6717
+ if (req.method === "GET" && path === "/health") {
6718
+ res.writeHead(200, { "content-type": "application/json" });
6719
+ res.end(JSON.stringify({ status: "ok", name }));
6720
+ return;
6721
+ }
6722
+ if (path === "/mcp") {
6723
+ await handleMcpRequest(req, res);
6724
+ return;
6725
+ }
6726
+ res.writeHead(404, { "content-type": "application/json" });
6727
+ res.end(JSON.stringify({ error: "Not found" }));
6728
+ });
6729
+ httpServer.listen(port, host, () => {
6730
+ const address = httpServer.address();
6731
+ const boundPort = typeof address === "object" && address ? address.port : port;
6732
+ console.error(`machines-mcp HTTP listening on http://${host}:${boundPort}`);
6733
+ });
6734
+ return httpServer;
6735
+ }
6736
+
6737
+ // src/mcp/index.ts
5865
6738
  function printHelp() {
5866
6739
  console.log(`Usage: machines-mcp [options]
5867
6740
 
5868
- MCP server for machine fleet management tools (stdio transport)
6741
+ MCP server for machine fleet management tools (stdio transport by default)
5869
6742
 
5870
6743
  Options:
6744
+ --http Start Streamable HTTP transport on 127.0.0.1 (or MCP_HTTP=1)
6745
+ --port <n> HTTP port (default: 8821, or MCP_HTTP_PORT env)
5871
6746
  -V, --version output the version number
5872
6747
  -h, --help display help for command`);
5873
6748
  }
@@ -5877,9 +6752,13 @@ if (args.includes("--help") || args.includes("-h")) {
5877
6752
  process.exit(0);
5878
6753
  }
5879
6754
  if (args.includes("--version") || args.includes("-V")) {
5880
- console.log(getPkgVersion());
6755
+ console.log(getPackageVersion());
5881
6756
  process.exit(0);
5882
6757
  }
5883
- var server = createMcpServer(getPkgVersion());
5884
- var transport = new StdioServerTransport;
5885
- await server.connect(transport);
6758
+ if (isHttpMode(args)) {
6759
+ startHttpServer({ port: resolveHttpPort(args) });
6760
+ } else {
6761
+ const server = buildServer();
6762
+ const transport = new StdioServerTransport;
6763
+ await server.connect(transport);
6764
+ }