@hasna/machines 0.0.1 → 0.0.3

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/index.js CHANGED
@@ -20335,17 +20335,95 @@ function runBackup(bucket, prefix = "machines", options = {}) {
20335
20335
  executed
20336
20336
  };
20337
20337
  }
20338
+ // src/remote.ts
20339
+ import { spawnSync as spawnSync2 } from "child_process";
20340
+
20341
+ // src/commands/ssh.ts
20342
+ import { spawnSync } from "child_process";
20343
+ function envReachableHosts() {
20344
+ const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
20345
+ return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
20346
+ }
20347
+ function isReachable(host) {
20348
+ const overrides = envReachableHosts();
20349
+ if (overrides.size > 0) {
20350
+ return overrides.has(host);
20351
+ }
20352
+ const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
20353
+ stdio: "ignore"
20354
+ });
20355
+ return probe.status === 0;
20356
+ }
20357
+ function resolveSshTarget(machineId) {
20358
+ const machine = getManifestMachine(machineId);
20359
+ if (!machine) {
20360
+ throw new Error(`Machine not found in manifest: ${machineId}`);
20361
+ }
20362
+ const current = detectCurrentMachineManifest();
20363
+ if (machine.id === current.id) {
20364
+ return {
20365
+ machineId,
20366
+ target: "localhost",
20367
+ route: "local"
20368
+ };
20369
+ }
20370
+ const lanTarget = machine.sshAddress || machine.hostname || machine.id;
20371
+ const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
20372
+ const route = isReachable(lanTarget) ? "lan" : "tailscale";
20373
+ return {
20374
+ machineId,
20375
+ target: route === "lan" ? lanTarget : tailscaleTarget,
20376
+ route
20377
+ };
20378
+ }
20379
+ function buildSshCommand(machineId, remoteCommand) {
20380
+ const resolved = resolveSshTarget(machineId);
20381
+ return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
20382
+ }
20383
+
20384
+ // src/remote.ts
20385
+ function runMachineCommand(machineId, command) {
20386
+ const localMachineId = getLocalMachineId();
20387
+ const isLocal = machineId === localMachineId;
20388
+ const route = isLocal ? "local" : resolveSshTarget(machineId).route;
20389
+ const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
20390
+ const result = spawnSync2("bash", ["-lc", shellCommand], {
20391
+ encoding: "utf8",
20392
+ env: process.env
20393
+ });
20394
+ return {
20395
+ machineId,
20396
+ source: route,
20397
+ stdout: result.stdout || "",
20398
+ stderr: result.stderr || "",
20399
+ exitCode: result.status ?? 1
20400
+ };
20401
+ }
20402
+
20338
20403
  // src/commands/apps.ts
20339
20404
  function getPackageName(app) {
20340
20405
  return app.packageName || app.name;
20341
20406
  }
20407
+ function getAppManager(machine, app) {
20408
+ if (app.manager)
20409
+ return app.manager;
20410
+ if (machine.platform === "macos")
20411
+ return "brew";
20412
+ if (machine.platform === "windows")
20413
+ return "winget";
20414
+ return "apt";
20415
+ }
20416
+ function shellQuote(value) {
20417
+ return `'${value.replace(/'/g, `'\\''`)}'`;
20418
+ }
20342
20419
  function buildAppCommand(machine, app) {
20343
20420
  const packageName = getPackageName(app);
20344
- if (app.manager === "custom") {
20421
+ const manager = getAppManager(machine, app);
20422
+ if (manager === "custom") {
20345
20423
  return packageName;
20346
20424
  }
20347
20425
  if (machine.platform === "macos") {
20348
- if (app.manager === "cask") {
20426
+ if (manager === "cask") {
20349
20427
  return `brew install --cask ${packageName}`;
20350
20428
  }
20351
20429
  return `brew install ${packageName}`;
@@ -20355,18 +20433,48 @@ function buildAppCommand(machine, app) {
20355
20433
  }
20356
20434
  return `sudo apt-get install -y ${packageName}`;
20357
20435
  }
20436
+ function buildAppProbeCommand(machine, app) {
20437
+ const packageName = shellQuote(getPackageName(app));
20438
+ const manager = getAppManager(machine, app);
20439
+ if (manager === "custom") {
20440
+ return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
20441
+ }
20442
+ if (machine.platform === "macos") {
20443
+ if (manager === "cask") {
20444
+ return `if brew list --cask ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
20445
+ }
20446
+ return `if brew list --versions ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion='; brew list --versions ${packageName} | awk '{print $2}'; printf '\\n'; else printf 'installed=0\\n'; fi`;
20447
+ }
20448
+ if (machine.platform === "windows") {
20449
+ return `if winget list --id ${packageName} --exact >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
20450
+ }
20451
+ return `if dpkg-query -W -f='${"${Version}"}' ${packageName} >/tmp/machines-app-version 2>/dev/null; then printf 'installed=1\\nversion='; cat /tmp/machines-app-version; printf '\\n'; rm -f /tmp/machines-app-version; else printf 'installed=0\\n'; fi`;
20452
+ }
20358
20453
  function buildAppSteps(machine) {
20359
20454
  return (machine.apps || []).map((app) => ({
20360
20455
  id: `app-${app.name}`,
20361
20456
  title: `Install ${app.name} on ${machine.id}`,
20362
20457
  command: buildAppCommand(machine, app),
20363
- manager: app.manager === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
20458
+ manager: getAppManager(machine, app) === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
20364
20459
  privileged: machine.platform === "linux"
20365
20460
  }));
20366
20461
  }
20367
20462
  function resolveMachine(machineId) {
20368
20463
  return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
20369
20464
  }
20465
+ function parseProbeOutput(app, machine, stdout) {
20466
+ const lines = stdout.trim().split(`
20467
+ `).filter(Boolean);
20468
+ const installedLine = lines.find((line) => line.startsWith("installed="));
20469
+ const versionLine = lines.find((line) => line.startsWith("version="));
20470
+ return {
20471
+ name: app.name,
20472
+ packageName: getPackageName(app),
20473
+ manager: getAppManager(machine, app),
20474
+ installed: installedLine === "installed=1",
20475
+ version: versionLine?.slice("version=".length) || undefined
20476
+ };
20477
+ }
20370
20478
  function listApps(machineId) {
20371
20479
  const machine = resolveMachine(machineId);
20372
20480
  return {
@@ -20383,6 +20491,26 @@ function buildAppsPlan(machineId) {
20383
20491
  executed: 0
20384
20492
  };
20385
20493
  }
20494
+ function getAppsStatus(machineId) {
20495
+ const machine = resolveMachine(machineId);
20496
+ const apps = (machine.apps || []).map((app) => {
20497
+ const probe = runMachineCommand(machine.id, buildAppProbeCommand(machine, app));
20498
+ return parseProbeOutput(app, machine, probe.stdout);
20499
+ });
20500
+ return {
20501
+ machineId: machine.id,
20502
+ source: apps.length > 0 ? runMachineCommand(machine.id, "true").source : machine.id === detectCurrentMachineManifest().id ? "local" : runMachineCommand(machine.id, "true").source,
20503
+ apps
20504
+ };
20505
+ }
20506
+ function diffApps(machineId) {
20507
+ const status = getAppsStatus(machineId);
20508
+ return {
20509
+ ...status,
20510
+ missing: status.apps.filter((app) => !app.installed).map((app) => app.name),
20511
+ installed: status.apps.filter((app) => app.installed).map((app) => app.name)
20512
+ };
20513
+ }
20386
20514
  function runAppsInstall(machineId, options = {}) {
20387
20515
  const plan = buildAppsPlan(machineId);
20388
20516
  if (!options.apply)
@@ -20529,6 +20657,58 @@ function renderDomainMapping(domain) {
20529
20657
  keyPath: join8(getDataDir(), "certs", `${entry.domain}-key.pem`)
20530
20658
  };
20531
20659
  }
20660
+ // src/commands/doctor.ts
20661
+ function makeCheck(id, status, summary, detail) {
20662
+ return { id, status, summary, detail };
20663
+ }
20664
+ function parseKeyValueOutput(stdout) {
20665
+ return Object.fromEntries(stdout.trim().split(`
20666
+ `).map((line) => line.split("=")).filter((parts) => parts.length === 2).map(([key, value]) => [key, value]));
20667
+ }
20668
+ function buildDoctorCommand() {
20669
+ return [
20670
+ 'data_dir="${HASNA_MACHINES_DIR:-$HOME/.hasna/machines}"',
20671
+ 'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
20672
+ 'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
20673
+ 'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
20674
+ `printf 'manifest_path=%s\\n' "$manifest_path"`,
20675
+ `printf 'db_path=%s\\n' "$db_path"`,
20676
+ `printf 'notifications_path=%s\\n' "$notifications_path"`,
20677
+ `printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
20678
+ `printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
20679
+ `printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
20680
+ `printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
20681
+ `printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
20682
+ `printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
20683
+ `printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
20684
+ `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
20685
+ ].join("; ");
20686
+ }
20687
+ function runDoctor(machineId = getLocalMachineId()) {
20688
+ const manifest = readManifest();
20689
+ const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
20690
+ const details = parseKeyValueOutput(commandChecks.stdout);
20691
+ const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
20692
+ const checks = [
20693
+ makeCheck("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(machineInManifest) : `No manifest entry for ${machineId}`),
20694
+ makeCheck("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${details["manifest_path"] || "unknown"} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`),
20695
+ makeCheck("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${details["db_path"] || "unknown"} ${details["db_exists"] === "yes" ? "exists" : "missing"}`),
20696
+ makeCheck("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${details["notifications_path"] || "unknown"} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`),
20697
+ makeCheck("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
20698
+ makeCheck("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
20699
+ makeCheck("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
20700
+ makeCheck("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
20701
+ makeCheck("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
20702
+ ];
20703
+ return {
20704
+ machineId,
20705
+ source: commandChecks.source,
20706
+ manifestPath: details["manifest_path"],
20707
+ dbPath: details["db_path"],
20708
+ notificationsPath: details["notifications_path"],
20709
+ checks
20710
+ };
20711
+ }
20532
20712
  // src/commands/manifest.ts
20533
20713
  function manifestInit() {
20534
20714
  return writeManifest(getDefaultManifest(), getManifestPath());
@@ -20608,6 +20788,13 @@ var AI_CLI_PACKAGES = {
20608
20788
  codex: "@openai/codex",
20609
20789
  gemini: "@google/gemini-cli"
20610
20790
  };
20791
+ function getToolBinary(tool) {
20792
+ if (tool === "claude")
20793
+ return process.env["HASNA_MACHINES_CLAUDE_BINARY"] || "claude";
20794
+ if (tool === "codex")
20795
+ return process.env["HASNA_MACHINES_CODEX_BINARY"] || "codex";
20796
+ return process.env["HASNA_MACHINES_GEMINI_BINARY"] || "gemini";
20797
+ }
20611
20798
  function normalizeTools(tools) {
20612
20799
  if (!tools || tools.length === 0) {
20613
20800
  return ["claude", "codex", "gemini"];
@@ -20627,8 +20814,27 @@ function buildInstallSteps(machine, tools) {
20627
20814
  manager: "bun"
20628
20815
  }));
20629
20816
  }
20817
+ function resolveMachine2(machineId) {
20818
+ return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
20819
+ }
20820
+ function buildProbeCommand(tool) {
20821
+ const binary = getToolBinary(tool);
20822
+ return `if command -v ${binary} >/dev/null 2>&1; then printf 'installed=1\\nversion='; ${binary} --version 2>/dev/null | head -n 1; printf '\\n'; else printf 'installed=0\\n'; fi`;
20823
+ }
20824
+ function parseProbe(tool, stdout) {
20825
+ const lines = stdout.trim().split(`
20826
+ `).filter(Boolean);
20827
+ const installedLine = lines.find((line) => line.startsWith("installed="));
20828
+ const versionLine = lines.find((line) => line.startsWith("version="));
20829
+ return {
20830
+ tool,
20831
+ packageName: AI_CLI_PACKAGES[tool],
20832
+ installed: installedLine === "installed=1",
20833
+ version: versionLine?.slice("version=".length) || undefined
20834
+ };
20835
+ }
20630
20836
  function buildClaudeInstallPlan(machineId, tools) {
20631
- const machine = (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
20837
+ const machine = resolveMachine2(machineId);
20632
20838
  return {
20633
20839
  machineId: machine.id,
20634
20840
  mode: "plan",
@@ -20636,6 +20842,24 @@ function buildClaudeInstallPlan(machineId, tools) {
20636
20842
  executed: 0
20637
20843
  };
20638
20844
  }
20845
+ function getClaudeCliStatus(machineId, tools) {
20846
+ const machine = resolveMachine2(machineId);
20847
+ const normalizedTools = normalizeTools(tools);
20848
+ const route = runMachineCommand(machine.id, "true").source;
20849
+ return {
20850
+ machineId: machine.id,
20851
+ source: route,
20852
+ tools: normalizedTools.map((tool) => parseProbe(tool, runMachineCommand(machine.id, buildProbeCommand(tool)).stdout))
20853
+ };
20854
+ }
20855
+ function diffClaudeCli(machineId, tools) {
20856
+ const status = getClaudeCliStatus(machineId, tools);
20857
+ return {
20858
+ ...status,
20859
+ missing: status.tools.filter((tool) => !tool.installed).map((tool) => tool.tool),
20860
+ installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
20861
+ };
20862
+ }
20639
20863
  function runClaudeInstall(machineId, tools, options = {}) {
20640
20864
  const plan = buildClaudeInstallPlan(machineId, tools);
20641
20865
  if (!options.apply)
@@ -20746,6 +20970,138 @@ var notificationConfigSchema = exports_external2.object({
20746
20970
  function sortChannels(channels) {
20747
20971
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
20748
20972
  }
20973
+ function shellQuote2(value) {
20974
+ return `'${value.replace(/'/g, `'\\''`)}'`;
20975
+ }
20976
+ function hasCommand(binary) {
20977
+ const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
20978
+ stdout: "ignore",
20979
+ stderr: "ignore",
20980
+ env: process.env
20981
+ });
20982
+ return result.exitCode === 0;
20983
+ }
20984
+ function buildNotificationPreview(channel, event, message) {
20985
+ if (channel.type === "email") {
20986
+ return `send email to ${channel.target}: [${event}] ${message}`;
20987
+ }
20988
+ if (channel.type === "webhook") {
20989
+ return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
20990
+ }
20991
+ return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
20992
+ }
20993
+ async function dispatchEmail(channel, event, message) {
20994
+ const subject = `[${event}] machines notification`;
20995
+ const body = `To: ${channel.target}
20996
+ Subject: ${subject}
20997
+ Content-Type: text/plain; charset=utf-8
20998
+
20999
+ ${message}
21000
+ `;
21001
+ if (hasCommand("sendmail")) {
21002
+ const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
21003
+ stdin: new TextEncoder().encode(body),
21004
+ stdout: "pipe",
21005
+ stderr: "pipe",
21006
+ env: process.env
21007
+ });
21008
+ if (result.exitCode !== 0) {
21009
+ throw new Error(result.stderr.toString().trim() || `sendmail exited with ${result.exitCode}`);
21010
+ }
21011
+ return {
21012
+ channelId: channel.id,
21013
+ event,
21014
+ delivered: true,
21015
+ transport: channel.type,
21016
+ detail: `Delivered via sendmail to ${channel.target}`
21017
+ };
21018
+ }
21019
+ if (hasCommand("mail")) {
21020
+ const command = `printf %s ${shellQuote2(message)} | mail -s ${shellQuote2(subject)} ${shellQuote2(channel.target)}`;
21021
+ const result = Bun.spawnSync(["bash", "-lc", command], {
21022
+ stdout: "pipe",
21023
+ stderr: "pipe",
21024
+ env: process.env
21025
+ });
21026
+ if (result.exitCode !== 0) {
21027
+ throw new Error(result.stderr.toString().trim() || `mail exited with ${result.exitCode}`);
21028
+ }
21029
+ return {
21030
+ channelId: channel.id,
21031
+ event,
21032
+ delivered: true,
21033
+ transport: channel.type,
21034
+ detail: `Delivered via mail to ${channel.target}`
21035
+ };
21036
+ }
21037
+ throw new Error("No local email transport available. Install sendmail or mail.");
21038
+ }
21039
+ async function dispatchWebhook(channel, event, message) {
21040
+ const response = await fetch(channel.target, {
21041
+ method: "POST",
21042
+ headers: {
21043
+ "content-type": "application/json"
21044
+ },
21045
+ body: JSON.stringify({
21046
+ channelId: channel.id,
21047
+ event,
21048
+ message,
21049
+ sentAt: new Date().toISOString()
21050
+ })
21051
+ });
21052
+ if (!response.ok) {
21053
+ const text = await response.text();
21054
+ throw new Error(`Webhook responded ${response.status}: ${text || response.statusText}`);
21055
+ }
21056
+ return {
21057
+ channelId: channel.id,
21058
+ event,
21059
+ delivered: true,
21060
+ transport: channel.type,
21061
+ detail: `Webhook accepted with HTTP ${response.status}`
21062
+ };
21063
+ }
21064
+ async function dispatchCommand(channel, event, message) {
21065
+ const result = Bun.spawnSync(["bash", "-lc", channel.target], {
21066
+ stdout: "pipe",
21067
+ stderr: "pipe",
21068
+ env: {
21069
+ ...process.env,
21070
+ HASNA_MACHINES_NOTIFICATION_CHANNEL: channel.id,
21071
+ HASNA_MACHINES_NOTIFICATION_EVENT: event,
21072
+ HASNA_MACHINES_NOTIFICATION_MESSAGE: message
21073
+ }
21074
+ });
21075
+ if (result.exitCode !== 0) {
21076
+ throw new Error(result.stderr.toString().trim() || `command exited with ${result.exitCode}`);
21077
+ }
21078
+ const stdout = result.stdout.toString().trim();
21079
+ return {
21080
+ channelId: channel.id,
21081
+ event,
21082
+ delivered: true,
21083
+ transport: channel.type,
21084
+ detail: stdout || "Command completed successfully"
21085
+ };
21086
+ }
21087
+ async function dispatchChannel(channel, event, message) {
21088
+ if (!channel.enabled) {
21089
+ return {
21090
+ channelId: channel.id,
21091
+ event,
21092
+ delivered: false,
21093
+ transport: channel.type,
21094
+ detail: "Channel is disabled"
21095
+ };
21096
+ }
21097
+ if (channel.type === "email") {
21098
+ return dispatchEmail(channel, event, message);
21099
+ }
21100
+ if (channel.type === "webhook") {
21101
+ return dispatchWebhook(channel, event, message);
21102
+ }
21103
+ return dispatchCommand(channel, event, message);
21104
+ }
20749
21105
  function getDefaultNotificationConfig() {
20750
21106
  return {
20751
21107
  version: 1,
@@ -20789,16 +21145,34 @@ function removeNotificationChannel(channelId) {
20789
21145
  channels: config.channels.filter((channel) => channel.id !== channelId)
20790
21146
  });
20791
21147
  }
20792
- function buildNotificationPreview(channel, event, message) {
20793
- if (channel.type === "email") {
20794
- return `send email to ${channel.target}: [${event}] ${message}`;
20795
- }
20796
- if (channel.type === "webhook") {
20797
- return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
21148
+ async function dispatchNotificationEvent(event, message, options = {}) {
21149
+ const channels = readNotificationConfig().channels.filter((channel) => {
21150
+ if (options.channelId && channel.id !== options.channelId) {
21151
+ return false;
21152
+ }
21153
+ return channel.events.includes(event) || event === "manual.test";
21154
+ });
21155
+ const deliveries = [];
21156
+ for (const channel of channels) {
21157
+ try {
21158
+ deliveries.push(await dispatchChannel(channel, event, message));
21159
+ } catch (error) {
21160
+ deliveries.push({
21161
+ channelId: channel.id,
21162
+ event,
21163
+ delivered: false,
21164
+ transport: channel.type,
21165
+ detail: error instanceof Error ? error.message : String(error)
21166
+ });
21167
+ }
20798
21168
  }
20799
- return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
21169
+ return {
21170
+ event,
21171
+ message,
21172
+ deliveries
21173
+ };
20800
21174
  }
20801
- function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
21175
+ async function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
20802
21176
  const channel = readNotificationConfig().channels.find((entry) => entry.id === channelId);
20803
21177
  if (!channel) {
20804
21178
  throw new Error(`Notification channel not found: ${channelId}`);
@@ -20809,76 +21183,24 @@ function testNotificationChannel(channelId, event = "manual.test", message = "ma
20809
21183
  channelId,
20810
21184
  mode: "plan",
20811
21185
  delivered: false,
20812
- preview
21186
+ preview,
21187
+ detail: "Preview only"
20813
21188
  };
20814
21189
  }
20815
21190
  if (!options.yes) {
20816
21191
  throw new Error("Notification test execution requires --yes.");
20817
21192
  }
20818
- if (channel.type === "command") {
20819
- const result = Bun.spawnSync(["bash", "-lc", preview], {
20820
- stdout: "pipe",
20821
- stderr: "pipe",
20822
- env: process.env
20823
- });
20824
- if (result.exitCode !== 0) {
20825
- throw new Error(`Notification command failed (${channel.id}): ${result.stderr.toString().trim()}`);
20826
- }
20827
- }
21193
+ const delivery = await dispatchChannel(channel, event, message);
20828
21194
  return {
20829
21195
  channelId,
20830
21196
  mode: "apply",
20831
- delivered: channel.enabled,
20832
- preview
20833
- };
20834
- }
20835
- // src/commands/ports.ts
20836
- import { spawnSync as spawnSync2 } from "child_process";
20837
-
20838
- // src/commands/ssh.ts
20839
- import { spawnSync } from "child_process";
20840
- function envReachableHosts() {
20841
- const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
20842
- return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
20843
- }
20844
- function isReachable(host) {
20845
- const overrides = envReachableHosts();
20846
- if (overrides.size > 0) {
20847
- return overrides.has(host);
20848
- }
20849
- const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
20850
- stdio: "ignore"
20851
- });
20852
- return probe.status === 0;
20853
- }
20854
- function resolveSshTarget(machineId) {
20855
- const machine = getManifestMachine(machineId);
20856
- if (!machine) {
20857
- throw new Error(`Machine not found in manifest: ${machineId}`);
20858
- }
20859
- const current = detectCurrentMachineManifest();
20860
- if (machine.id === current.id) {
20861
- return {
20862
- machineId,
20863
- target: "localhost",
20864
- route: "local"
20865
- };
20866
- }
20867
- const lanTarget = machine.sshAddress || machine.hostname || machine.id;
20868
- const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
20869
- const route = isReachable(lanTarget) ? "lan" : "tailscale";
20870
- return {
20871
- machineId,
20872
- target: route === "lan" ? lanTarget : tailscaleTarget,
20873
- route
21197
+ delivered: delivery.delivered,
21198
+ preview,
21199
+ detail: delivery.detail
20874
21200
  };
20875
21201
  }
20876
- function buildSshCommand(machineId, remoteCommand) {
20877
- const resolved = resolveSshTarget(machineId);
20878
- return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
20879
- }
20880
-
20881
21202
  // src/commands/ports.ts
21203
+ import { spawnSync as spawnSync3 } from "child_process";
20882
21204
  function parseSsOutput(output) {
20883
21205
  return output.trim().split(`
20884
21206
  `).map((line) => line.trim()).filter(Boolean).map((line) => {
@@ -20920,7 +21242,7 @@ function listPorts(machineId) {
20920
21242
  const isLocal = targetMachineId === getLocalMachineId();
20921
21243
  const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
20922
21244
  const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
20923
- const result = spawnSync2("bash", ["-lc", command], { encoding: "utf8" });
21245
+ const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
20924
21246
  if (result.status !== 0) {
20925
21247
  throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
20926
21248
  }
@@ -20930,6 +21252,24 @@ function listPorts(machineId) {
20930
21252
  listeners: parsePortOutput(result.stdout, format)
20931
21253
  };
20932
21254
  }
21255
+ // src/version.ts
21256
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
21257
+ import { dirname as dirname4, join as join9 } from "path";
21258
+ import { fileURLToPath } from "url";
21259
+ function getPackageVersion() {
21260
+ try {
21261
+ const here = dirname4(fileURLToPath(import.meta.url));
21262
+ const candidates = [join9(here, "..", "package.json"), join9(here, "..", "..", "package.json")];
21263
+ const pkgPath = candidates.find((candidate) => existsSync8(candidate));
21264
+ if (!pkgPath) {
21265
+ return "0.0.0";
21266
+ }
21267
+ return JSON.parse(readFileSync5(pkgPath, "utf8")).version || "0.0.0";
21268
+ } catch {
21269
+ return "0.0.0";
21270
+ }
21271
+ }
21272
+
20933
21273
  // src/commands/status.ts
20934
21274
  function getStatus() {
20935
21275
  const manifest = readManifest();
@@ -20973,13 +21313,27 @@ function getServeInfo(options = {}) {
20973
21313
  host,
20974
21314
  port,
20975
21315
  url: `http://${host}:${port}`,
20976
- routes: ["/", "/health", "/api/status", "/api/manifest", "/api/notifications"]
21316
+ routes: [
21317
+ "/",
21318
+ "/health",
21319
+ "/api/status",
21320
+ "/api/manifest",
21321
+ "/api/notifications",
21322
+ "/api/doctor",
21323
+ "/api/self-test",
21324
+ "/api/apps/status",
21325
+ "/api/apps/diff",
21326
+ "/api/install-claude/status",
21327
+ "/api/install-claude/diff",
21328
+ "/api/notifications/test"
21329
+ ]
20977
21330
  };
20978
21331
  }
20979
21332
  function renderDashboardHtml() {
20980
21333
  const status = getStatus();
20981
21334
  const manifest = manifestList();
20982
21335
  const notifications = listNotificationChannels();
21336
+ const doctor = runDoctor();
20983
21337
  return `<!doctype html>
20984
21338
  <html lang="en">
20985
21339
  <head>
@@ -20995,12 +21349,14 @@ function renderDashboardHtml() {
20995
21349
  .card { background: #121933; border: 1px solid #243057; border-radius: 16px; padding: 20px; }
20996
21350
  .stat { font-size: 32px; font-weight: 700; margin-top: 8px; }
20997
21351
  table { width: 100%; border-collapse: collapse; }
20998
- th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; }
21352
+ th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; vertical-align: top; }
20999
21353
  code { color: #9ed0ff; }
21000
21354
  .badge { display: inline-block; border-radius: 999px; padding: 4px 10px; font-size: 12px; }
21001
- .online { background: #12351f; color: #74f0a7; }
21002
- .offline { background: #3b1a1a; color: #ff8c8c; }
21003
- .unknown { background: #2f2b16; color: #ffd76a; }
21355
+ .online, .ok { background: #12351f; color: #74f0a7; }
21356
+ .offline, .fail { background: #3b1a1a; color: #ff8c8c; }
21357
+ .unknown, .warn { background: #2f2b16; color: #ffd76a; }
21358
+ ul { margin: 8px 0 0; padding-left: 18px; }
21359
+ .muted { color: #9fb0d9; }
21004
21360
  </style>
21005
21361
  </head>
21006
21362
  <body>
@@ -21010,6 +21366,7 @@ function renderDashboardHtml() {
21010
21366
  <section class="card"><div>Manifest machines</div><div class="stat">${status.manifestMachineCount}</div></section>
21011
21367
  <section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
21012
21368
  <section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
21369
+ <section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
21013
21370
  </div>
21014
21371
 
21015
21372
  <section class="card" style="margin-top:16px">
@@ -21027,6 +21384,25 @@ function renderDashboardHtml() {
21027
21384
  </table>
21028
21385
  </section>
21029
21386
 
21387
+ <section class="card" style="margin-top:16px">
21388
+ <h2>Doctor</h2>
21389
+ <table>
21390
+ <thead><tr><th>Check</th><th>Status</th><th>Detail</th></tr></thead>
21391
+ <tbody>
21392
+ ${doctor.checks.map((entry) => `<tr>
21393
+ <td>${escapeHtml(entry.summary)}</td>
21394
+ <td><span class="badge ${escapeHtml(entry.status)}">${escapeHtml(entry.status)}</span></td>
21395
+ <td class="muted">${escapeHtml(entry.detail)}</td>
21396
+ </tr>`).join("")}
21397
+ </tbody>
21398
+ </table>
21399
+ </section>
21400
+
21401
+ <section class="card" style="margin-top:16px">
21402
+ <h2>Self Test</h2>
21403
+ <p class="muted">Use <code>/api/self-test</code> for the full smoke-check payload.</p>
21404
+ </section>
21405
+
21030
21406
  <section class="card" style="margin-top:16px">
21031
21407
  <h2>Manifest</h2>
21032
21408
  <pre>${escapeHtml(JSON.stringify(manifest, null, 2))}</pre>
@@ -21035,13 +21411,25 @@ function renderDashboardHtml() {
21035
21411
  </body>
21036
21412
  </html>`;
21037
21413
  }
21414
+ async function parseJsonBody(request) {
21415
+ try {
21416
+ return await request.json();
21417
+ } catch {
21418
+ return {};
21419
+ }
21420
+ }
21421
+ function jsonError(message, status = 400) {
21422
+ return Response.json({ error: message }, { status });
21423
+ }
21038
21424
  function startDashboardServer(options = {}) {
21039
21425
  const info = getServeInfo(options);
21040
21426
  return Bun.serve({
21041
21427
  hostname: info.host,
21042
21428
  port: info.port,
21043
- fetch(request) {
21429
+ async fetch(request) {
21044
21430
  const url = new URL(request.url);
21431
+ const machineId = url.searchParams.get("machine") || undefined;
21432
+ const tools = url.searchParams.get("tools")?.split(",").map((value) => value.trim()).filter(Boolean);
21045
21433
  if (url.pathname === "/health") {
21046
21434
  return Response.json({ ok: true, ...getServeInfo(options) });
21047
21435
  }
@@ -21054,6 +21442,43 @@ function startDashboardServer(options = {}) {
21054
21442
  if (url.pathname === "/api/notifications") {
21055
21443
  return Response.json(listNotificationChannels());
21056
21444
  }
21445
+ if (url.pathname === "/api/doctor") {
21446
+ return Response.json(runDoctor(machineId));
21447
+ }
21448
+ if (url.pathname === "/api/self-test") {
21449
+ return Response.json(runSelfTest());
21450
+ }
21451
+ if (url.pathname === "/api/apps/status") {
21452
+ return Response.json(getAppsStatus(machineId));
21453
+ }
21454
+ if (url.pathname === "/api/apps/diff") {
21455
+ return Response.json(diffApps(machineId));
21456
+ }
21457
+ if (url.pathname === "/api/install-claude/status") {
21458
+ return Response.json(getClaudeCliStatus(machineId, tools));
21459
+ }
21460
+ if (url.pathname === "/api/install-claude/diff") {
21461
+ return Response.json(diffClaudeCli(machineId, tools));
21462
+ }
21463
+ if (url.pathname === "/api/notifications/test") {
21464
+ if (request.method !== "POST") {
21465
+ return jsonError("Use POST for notification tests.", 405);
21466
+ }
21467
+ const body = await parseJsonBody(request);
21468
+ const channelId = typeof body["channelId"] === "string" ? body["channelId"] : undefined;
21469
+ if (!channelId) {
21470
+ return jsonError("channelId is required.");
21471
+ }
21472
+ const event = typeof body["event"] === "string" ? body["event"] : undefined;
21473
+ const message = typeof body["message"] === "string" ? body["message"] : undefined;
21474
+ const apply = body["apply"] === true;
21475
+ const yes = body["yes"] === true;
21476
+ try {
21477
+ return Response.json(await testNotificationChannel(channelId, event, message, { apply, yes }));
21478
+ } catch (error) {
21479
+ return jsonError(error instanceof Error ? error.message : String(error));
21480
+ }
21481
+ }
21057
21482
  return new Response(renderDashboardHtml(), {
21058
21483
  headers: {
21059
21484
  "content-type": "text/html; charset=utf-8"
@@ -21062,6 +21487,36 @@ function startDashboardServer(options = {}) {
21062
21487
  }
21063
21488
  });
21064
21489
  }
21490
+
21491
+ // src/commands/self-test.ts
21492
+ function check(id, status, summary, detail) {
21493
+ return { id, status, summary, detail };
21494
+ }
21495
+ function runSelfTest() {
21496
+ const version = getPackageVersion();
21497
+ const status = getStatus();
21498
+ const doctor = runDoctor();
21499
+ const serveInfo = getServeInfo();
21500
+ const html = renderDashboardHtml();
21501
+ const notifications = listNotificationChannels();
21502
+ const apps = listApps(status.machineId);
21503
+ const appsDiff = diffApps(status.machineId);
21504
+ const cliPlan = buildClaudeInstallPlan(status.machineId);
21505
+ return {
21506
+ machineId: getLocalMachineId(),
21507
+ checks: [
21508
+ check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
21509
+ check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
21510
+ check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
21511
+ check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
21512
+ check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
21513
+ check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
21514
+ check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
21515
+ check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
21516
+ check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
21517
+ ]
21518
+ };
21519
+ }
21065
21520
  // src/commands/setup.ts
21066
21521
  function quote3(value) {
21067
21522
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -21177,7 +21632,7 @@ function runSetup(machineId, options = {}) {
21177
21632
  return summary;
21178
21633
  }
21179
21634
  // src/commands/sync.ts
21180
- import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync as copyFileSync2 } from "fs";
21635
+ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync6, symlinkSync, copyFileSync as copyFileSync2 } from "fs";
21181
21636
  function quote4(value) {
21182
21637
  return `'${value.replace(/'/g, `'\\''`)}'`;
21183
21638
  }
@@ -21208,12 +21663,12 @@ function packageInstallCommand(machine, packageName, manager = machine.platform
21208
21663
  function detectPackageActions(machine) {
21209
21664
  return (machine.packages || []).map((pkg, index) => {
21210
21665
  const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
21211
- const check = Bun.spawnSync(["bash", "-lc", packageCheckCommand(machine, pkg.name, manager)], {
21666
+ const check2 = Bun.spawnSync(["bash", "-lc", packageCheckCommand(machine, pkg.name, manager)], {
21212
21667
  stdout: "ignore",
21213
21668
  stderr: "ignore",
21214
21669
  env: process.env
21215
21670
  });
21216
- const installed = check.exitCode === 0;
21671
+ const installed = check2.exitCode === 0;
21217
21672
  return {
21218
21673
  id: `package-${index + 1}`,
21219
21674
  title: `${installed ? "Package present" : "Install package"} ${pkg.name}`,
@@ -21225,15 +21680,15 @@ function detectPackageActions(machine) {
21225
21680
  }
21226
21681
  function detectFileActions(machine) {
21227
21682
  return (machine.files || []).map((file, index) => {
21228
- const sourceExists = existsSync8(file.source);
21229
- const targetExists = existsSync8(file.target);
21683
+ const sourceExists = existsSync9(file.source);
21684
+ const targetExists = existsSync9(file.target);
21230
21685
  let status = "missing";
21231
21686
  if (sourceExists && targetExists) {
21232
21687
  if (file.mode === "symlink") {
21233
21688
  status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
21234
21689
  } else {
21235
- const source = readFileSync5(file.source, "utf8");
21236
- const target = readFileSync5(file.target, "utf8");
21690
+ const source = readFileSync6(file.source, "utf8");
21691
+ const target = readFileSync6(file.target, "utf8");
21237
21692
  status = source === target ? "ok" : "drifted";
21238
21693
  }
21239
21694
  }
@@ -25384,7 +25839,7 @@ var ZodType3 = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
25384
25839
  inst.parseAsync = async (data, params) => parseAsync2(inst, data, params, { callee: inst.parseAsync });
25385
25840
  inst.safeParseAsync = async (data, params) => safeParseAsync3(inst, data, params);
25386
25841
  inst.spa = inst.safeParseAsync;
25387
- inst.refine = (check, params) => inst.check(refine(check, params));
25842
+ inst.refine = (check2, params) => inst.check(refine(check2, params));
25388
25843
  inst.superRefine = (refinement) => inst.check(superRefine(refinement));
25389
25844
  inst.overwrite = (fn) => inst.check(_overwrite(fn));
25390
25845
  inst.optional = () => optional(inst);
@@ -25928,7 +26383,7 @@ var ZodCustom = /* @__PURE__ */ $constructor("ZodCustom", (inst, def) => {
25928
26383
  $ZodCustom.init(inst, def);
25929
26384
  ZodType3.init(inst, def);
25930
26385
  });
25931
- function check(fn) {
26386
+ function check2(fn) {
25932
26387
  const ch = new $ZodCheck({
25933
26388
  check: "custom"
25934
26389
  });
@@ -25942,7 +26397,7 @@ function refine(fn, _params = {}) {
25942
26397
  return _refine(ZodCustom, fn, _params);
25943
26398
  }
25944
26399
  function superRefine(fn) {
25945
- const ch = check((payload) => {
26400
+ const ch = check2((payload) => {
25946
26401
  payload.addIssue = (issue2) => {
25947
26402
  if (typeof issue2 === "string") {
25948
26403
  payload.issues.push(exports_util.issue(issue2, payload.value, ch._zod.def));
@@ -26941,38 +27396,38 @@ function parseBigintDef(def, refs) {
26941
27396
  };
26942
27397
  if (!def.checks)
26943
27398
  return res;
26944
- for (const check2 of def.checks) {
26945
- switch (check2.kind) {
27399
+ for (const check3 of def.checks) {
27400
+ switch (check3.kind) {
26946
27401
  case "min":
26947
27402
  if (refs.target === "jsonSchema7") {
26948
- if (check2.inclusive) {
26949
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
27403
+ if (check3.inclusive) {
27404
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
26950
27405
  } else {
26951
- setResponseValueAndErrors(res, "exclusiveMinimum", check2.value, check2.message, refs);
27406
+ setResponseValueAndErrors(res, "exclusiveMinimum", check3.value, check3.message, refs);
26952
27407
  }
26953
27408
  } else {
26954
- if (!check2.inclusive) {
27409
+ if (!check3.inclusive) {
26955
27410
  res.exclusiveMinimum = true;
26956
27411
  }
26957
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
27412
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
26958
27413
  }
26959
27414
  break;
26960
27415
  case "max":
26961
27416
  if (refs.target === "jsonSchema7") {
26962
- if (check2.inclusive) {
26963
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
27417
+ if (check3.inclusive) {
27418
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
26964
27419
  } else {
26965
- setResponseValueAndErrors(res, "exclusiveMaximum", check2.value, check2.message, refs);
27420
+ setResponseValueAndErrors(res, "exclusiveMaximum", check3.value, check3.message, refs);
26966
27421
  }
26967
27422
  } else {
26968
- if (!check2.inclusive) {
27423
+ if (!check3.inclusive) {
26969
27424
  res.exclusiveMaximum = true;
26970
27425
  }
26971
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
27426
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
26972
27427
  }
26973
27428
  break;
26974
27429
  case "multipleOf":
26975
- setResponseValueAndErrors(res, "multipleOf", check2.value, check2.message, refs);
27430
+ setResponseValueAndErrors(res, "multipleOf", check3.value, check3.message, refs);
26976
27431
  break;
26977
27432
  }
26978
27433
  }
@@ -27028,13 +27483,13 @@ var integerDateParser = (def, refs) => {
27028
27483
  if (refs.target === "openApi3") {
27029
27484
  return res;
27030
27485
  }
27031
- for (const check2 of def.checks) {
27032
- switch (check2.kind) {
27486
+ for (const check3 of def.checks) {
27487
+ switch (check3.kind) {
27033
27488
  case "min":
27034
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
27489
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
27035
27490
  break;
27036
27491
  case "max":
27037
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
27492
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
27038
27493
  break;
27039
27494
  }
27040
27495
  }
@@ -27152,125 +27607,125 @@ function parseStringDef(def, refs) {
27152
27607
  type: "string"
27153
27608
  };
27154
27609
  if (def.checks) {
27155
- for (const check2 of def.checks) {
27156
- switch (check2.kind) {
27610
+ for (const check3 of def.checks) {
27611
+ switch (check3.kind) {
27157
27612
  case "min":
27158
- setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check2.value) : check2.value, check2.message, refs);
27613
+ setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check3.value) : check3.value, check3.message, refs);
27159
27614
  break;
27160
27615
  case "max":
27161
- setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check2.value) : check2.value, check2.message, refs);
27616
+ setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check3.value) : check3.value, check3.message, refs);
27162
27617
  break;
27163
27618
  case "email":
27164
27619
  switch (refs.emailStrategy) {
27165
27620
  case "format:email":
27166
- addFormat(res, "email", check2.message, refs);
27621
+ addFormat(res, "email", check3.message, refs);
27167
27622
  break;
27168
27623
  case "format:idn-email":
27169
- addFormat(res, "idn-email", check2.message, refs);
27624
+ addFormat(res, "idn-email", check3.message, refs);
27170
27625
  break;
27171
27626
  case "pattern:zod":
27172
- addPattern(res, zodPatterns.email, check2.message, refs);
27627
+ addPattern(res, zodPatterns.email, check3.message, refs);
27173
27628
  break;
27174
27629
  }
27175
27630
  break;
27176
27631
  case "url":
27177
- addFormat(res, "uri", check2.message, refs);
27632
+ addFormat(res, "uri", check3.message, refs);
27178
27633
  break;
27179
27634
  case "uuid":
27180
- addFormat(res, "uuid", check2.message, refs);
27635
+ addFormat(res, "uuid", check3.message, refs);
27181
27636
  break;
27182
27637
  case "regex":
27183
- addPattern(res, check2.regex, check2.message, refs);
27638
+ addPattern(res, check3.regex, check3.message, refs);
27184
27639
  break;
27185
27640
  case "cuid":
27186
- addPattern(res, zodPatterns.cuid, check2.message, refs);
27641
+ addPattern(res, zodPatterns.cuid, check3.message, refs);
27187
27642
  break;
27188
27643
  case "cuid2":
27189
- addPattern(res, zodPatterns.cuid2, check2.message, refs);
27644
+ addPattern(res, zodPatterns.cuid2, check3.message, refs);
27190
27645
  break;
27191
27646
  case "startsWith":
27192
- addPattern(res, RegExp(`^${escapeLiteralCheckValue(check2.value, refs)}`), check2.message, refs);
27647
+ addPattern(res, RegExp(`^${escapeLiteralCheckValue(check3.value, refs)}`), check3.message, refs);
27193
27648
  break;
27194
27649
  case "endsWith":
27195
- addPattern(res, RegExp(`${escapeLiteralCheckValue(check2.value, refs)}$`), check2.message, refs);
27650
+ addPattern(res, RegExp(`${escapeLiteralCheckValue(check3.value, refs)}$`), check3.message, refs);
27196
27651
  break;
27197
27652
  case "datetime":
27198
- addFormat(res, "date-time", check2.message, refs);
27653
+ addFormat(res, "date-time", check3.message, refs);
27199
27654
  break;
27200
27655
  case "date":
27201
- addFormat(res, "date", check2.message, refs);
27656
+ addFormat(res, "date", check3.message, refs);
27202
27657
  break;
27203
27658
  case "time":
27204
- addFormat(res, "time", check2.message, refs);
27659
+ addFormat(res, "time", check3.message, refs);
27205
27660
  break;
27206
27661
  case "duration":
27207
- addFormat(res, "duration", check2.message, refs);
27662
+ addFormat(res, "duration", check3.message, refs);
27208
27663
  break;
27209
27664
  case "length":
27210
- setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check2.value) : check2.value, check2.message, refs);
27211
- setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check2.value) : check2.value, check2.message, refs);
27665
+ setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check3.value) : check3.value, check3.message, refs);
27666
+ setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check3.value) : check3.value, check3.message, refs);
27212
27667
  break;
27213
27668
  case "includes": {
27214
- addPattern(res, RegExp(escapeLiteralCheckValue(check2.value, refs)), check2.message, refs);
27669
+ addPattern(res, RegExp(escapeLiteralCheckValue(check3.value, refs)), check3.message, refs);
27215
27670
  break;
27216
27671
  }
27217
27672
  case "ip": {
27218
- if (check2.version !== "v6") {
27219
- addFormat(res, "ipv4", check2.message, refs);
27673
+ if (check3.version !== "v6") {
27674
+ addFormat(res, "ipv4", check3.message, refs);
27220
27675
  }
27221
- if (check2.version !== "v4") {
27222
- addFormat(res, "ipv6", check2.message, refs);
27676
+ if (check3.version !== "v4") {
27677
+ addFormat(res, "ipv6", check3.message, refs);
27223
27678
  }
27224
27679
  break;
27225
27680
  }
27226
27681
  case "base64url":
27227
- addPattern(res, zodPatterns.base64url, check2.message, refs);
27682
+ addPattern(res, zodPatterns.base64url, check3.message, refs);
27228
27683
  break;
27229
27684
  case "jwt":
27230
- addPattern(res, zodPatterns.jwt, check2.message, refs);
27685
+ addPattern(res, zodPatterns.jwt, check3.message, refs);
27231
27686
  break;
27232
27687
  case "cidr": {
27233
- if (check2.version !== "v6") {
27234
- addPattern(res, zodPatterns.ipv4Cidr, check2.message, refs);
27688
+ if (check3.version !== "v6") {
27689
+ addPattern(res, zodPatterns.ipv4Cidr, check3.message, refs);
27235
27690
  }
27236
- if (check2.version !== "v4") {
27237
- addPattern(res, zodPatterns.ipv6Cidr, check2.message, refs);
27691
+ if (check3.version !== "v4") {
27692
+ addPattern(res, zodPatterns.ipv6Cidr, check3.message, refs);
27238
27693
  }
27239
27694
  break;
27240
27695
  }
27241
27696
  case "emoji":
27242
- addPattern(res, zodPatterns.emoji(), check2.message, refs);
27697
+ addPattern(res, zodPatterns.emoji(), check3.message, refs);
27243
27698
  break;
27244
27699
  case "ulid": {
27245
- addPattern(res, zodPatterns.ulid, check2.message, refs);
27700
+ addPattern(res, zodPatterns.ulid, check3.message, refs);
27246
27701
  break;
27247
27702
  }
27248
27703
  case "base64": {
27249
27704
  switch (refs.base64Strategy) {
27250
27705
  case "format:binary": {
27251
- addFormat(res, "binary", check2.message, refs);
27706
+ addFormat(res, "binary", check3.message, refs);
27252
27707
  break;
27253
27708
  }
27254
27709
  case "contentEncoding:base64": {
27255
- setResponseValueAndErrors(res, "contentEncoding", "base64", check2.message, refs);
27710
+ setResponseValueAndErrors(res, "contentEncoding", "base64", check3.message, refs);
27256
27711
  break;
27257
27712
  }
27258
27713
  case "pattern:zod": {
27259
- addPattern(res, zodPatterns.base64, check2.message, refs);
27714
+ addPattern(res, zodPatterns.base64, check3.message, refs);
27260
27715
  break;
27261
27716
  }
27262
27717
  }
27263
27718
  break;
27264
27719
  }
27265
27720
  case "nanoid": {
27266
- addPattern(res, zodPatterns.nanoid, check2.message, refs);
27721
+ addPattern(res, zodPatterns.nanoid, check3.message, refs);
27267
27722
  }
27268
27723
  case "toLowerCase":
27269
27724
  case "toUpperCase":
27270
27725
  case "trim":
27271
27726
  break;
27272
27727
  default:
27273
- ((_) => {})(check2);
27728
+ ((_) => {})(check3);
27274
27729
  }
27275
27730
  }
27276
27731
  }
@@ -27639,42 +28094,42 @@ function parseNumberDef(def, refs) {
27639
28094
  };
27640
28095
  if (!def.checks)
27641
28096
  return res;
27642
- for (const check2 of def.checks) {
27643
- switch (check2.kind) {
28097
+ for (const check3 of def.checks) {
28098
+ switch (check3.kind) {
27644
28099
  case "int":
27645
28100
  res.type = "integer";
27646
- addErrorMessage(res, "type", check2.message, refs);
28101
+ addErrorMessage(res, "type", check3.message, refs);
27647
28102
  break;
27648
28103
  case "min":
27649
28104
  if (refs.target === "jsonSchema7") {
27650
- if (check2.inclusive) {
27651
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
28105
+ if (check3.inclusive) {
28106
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
27652
28107
  } else {
27653
- setResponseValueAndErrors(res, "exclusiveMinimum", check2.value, check2.message, refs);
28108
+ setResponseValueAndErrors(res, "exclusiveMinimum", check3.value, check3.message, refs);
27654
28109
  }
27655
28110
  } else {
27656
- if (!check2.inclusive) {
28111
+ if (!check3.inclusive) {
27657
28112
  res.exclusiveMinimum = true;
27658
28113
  }
27659
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
28114
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
27660
28115
  }
27661
28116
  break;
27662
28117
  case "max":
27663
28118
  if (refs.target === "jsonSchema7") {
27664
- if (check2.inclusive) {
27665
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
28119
+ if (check3.inclusive) {
28120
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
27666
28121
  } else {
27667
- setResponseValueAndErrors(res, "exclusiveMaximum", check2.value, check2.message, refs);
28122
+ setResponseValueAndErrors(res, "exclusiveMaximum", check3.value, check3.message, refs);
27668
28123
  }
27669
28124
  } else {
27670
- if (!check2.inclusive) {
28125
+ if (!check3.inclusive) {
27671
28126
  res.exclusiveMaximum = true;
27672
28127
  }
27673
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
28128
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
27674
28129
  }
27675
28130
  break;
27676
28131
  case "multipleOf":
27677
- setResponseValueAndErrors(res, "multipleOf", check2.value, check2.message, refs);
28132
+ setResponseValueAndErrors(res, "multipleOf", check3.value, check3.message, refs);
27678
28133
  break;
27679
28134
  }
27680
28135
  }
@@ -30238,7 +30693,11 @@ var EMPTY_COMPLETION_RESULT = {
30238
30693
  // src/mcp/server.ts
30239
30694
  var MACHINE_MCP_TOOL_NAMES = [
30240
30695
  "machines_status",
30696
+ "machines_doctor",
30697
+ "machines_self_test",
30241
30698
  "machines_apps_list",
30699
+ "machines_apps_status",
30700
+ "machines_apps_diff",
30242
30701
  "machines_apps_plan",
30243
30702
  "machines_apps_apply",
30244
30703
  "machines_manifest",
@@ -30254,6 +30713,8 @@ var MACHINE_MCP_TOOL_NAMES = [
30254
30713
  "machines_diff",
30255
30714
  "machines_install_tailscale_preview",
30256
30715
  "machines_install_tailscale_apply",
30716
+ "machines_install_claude_status",
30717
+ "machines_install_claude_diff",
30257
30718
  "machines_install_claude_preview",
30258
30719
  "machines_install_claude_apply",
30259
30720
  "machines_ssh_resolve",
@@ -30268,30 +30729,25 @@ var MACHINE_MCP_TOOL_NAMES = [
30268
30729
  "machines_notifications_add",
30269
30730
  "machines_notifications_list",
30270
30731
  "machines_notifications_test",
30732
+ "machines_notifications_dispatch",
30271
30733
  "machines_notifications_remove",
30272
30734
  "machines_serve_info",
30273
30735
  "machines_serve_dashboard"
30274
30736
  ];
30275
30737
  function createMcpServer(version2) {
30276
- const server = new McpServer({
30277
- name: "machines",
30278
- version: version2
30279
- });
30738
+ const server = new McpServer({ name: "machines", version: version2 });
30280
30739
  server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
30281
30740
  content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
30282
30741
  }));
30283
- server.tool("machines_apps_list", "List manifest-managed apps for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
30284
- content: [{ type: "text", text: JSON.stringify(listApps(machine_id), null, 2) }]
30285
- }));
30286
- server.tool("machines_apps_plan", "Preview app install steps for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
30287
- content: [{ type: "text", text: JSON.stringify(buildAppsPlan(machine_id), null, 2) }]
30288
- }));
30289
- server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", {
30290
- machine_id: exports_external2.string().optional().describe("Machine identifier"),
30291
- yes: exports_external2.boolean().describe("Confirmation flag for execution")
30292
- }, async ({ machine_id, yes }) => ({
30293
- content: [{ type: "text", text: JSON.stringify(runAppsInstall(machine_id, { apply: true, yes }), null, 2) }]
30742
+ server.tool("machines_doctor", "Run machine preflight checks.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(runDoctor(machine_id), null, 2) }] }));
30743
+ server.tool("machines_self_test", "Run local package smoke checks.", {}, async () => ({
30744
+ content: [{ type: "text", text: JSON.stringify(runSelfTest(), null, 2) }]
30294
30745
  }));
30746
+ server.tool("machines_apps_list", "List manifest-managed apps for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(listApps(machine_id), null, 2) }] }));
30747
+ server.tool("machines_apps_status", "Check installed state for manifest-managed apps.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(getAppsStatus(machine_id), null, 2) }] }));
30748
+ server.tool("machines_apps_diff", "Show missing and installed manifest-managed apps.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(diffApps(machine_id), null, 2) }] }));
30749
+ server.tool("machines_apps_plan", "Preview app install steps for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildAppsPlan(machine_id), null, 2) }] }));
30750
+ server.tool("machines_apps_apply", "Install manifest-managed apps for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier"), yes: exports_external2.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runAppsInstall(machine_id, { apply: true, yes }), null, 2) }] }));
30295
30751
  server.tool("machines_manifest", "Read the current fleet manifest.", {}, async () => ({
30296
30752
  content: [{ type: "text", text: JSON.stringify(manifestList(), null, 2) }]
30297
30753
  }));
@@ -30301,45 +30757,33 @@ function createMcpServer(version2) {
30301
30757
  server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", {}, async () => ({
30302
30758
  content: [{ type: "text", text: JSON.stringify(manifestBootstrapCurrentMachine(), null, 2) }]
30303
30759
  }));
30304
- server.tool("machines_manifest_get", "Read a single machine from the fleet manifest.", { machine_id: exports_external2.string().describe("Machine identifier") }, async ({ machine_id }) => ({
30305
- content: [{ type: "text", text: JSON.stringify(manifestGet(machine_id), null, 2) }]
30306
- }));
30307
- server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external2.string().describe("Machine identifier") }, async ({ machine_id }) => ({
30308
- content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }]
30309
- }));
30760
+ server.tool("machines_manifest_get", "Read a single machine from the fleet manifest.", { machine_id: exports_external2.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestGet(machine_id), null, 2) }] }));
30761
+ server.tool("machines_manifest_remove", "Remove a single machine from the fleet manifest.", { machine_id: exports_external2.string().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(manifestRemove(machine_id), null, 2) }] }));
30310
30762
  server.tool("machines_agent_status", "List current machine agent heartbeats.", {}, async () => ({
30311
30763
  content: [{ type: "text", text: JSON.stringify(getAgentStatus(), null, 2) }]
30312
30764
  }));
30313
- server.tool("machines_setup_preview", "Preview setup actions for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
30314
- content: [{ type: "text", text: JSON.stringify(buildSetupPlan(machine_id), null, 2) }]
30315
- }));
30316
- server.tool("machines_setup_apply", "Execute setup actions for a machine.", {
30317
- machine_id: exports_external2.string().optional().describe("Machine identifier"),
30318
- yes: exports_external2.boolean().describe("Confirmation flag for execution")
30319
- }, async ({ machine_id, yes }) => ({
30320
- content: [{ type: "text", text: JSON.stringify(runSetup(machine_id, { apply: true, yes }), null, 2) }]
30321
- }));
30322
- server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
30323
- content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }]
30324
- }));
30325
- server.tool("machines_sync_apply", "Execute sync actions for a machine.", {
30326
- machine_id: exports_external2.string().optional().describe("Machine identifier"),
30327
- yes: exports_external2.boolean().describe("Confirmation flag for execution")
30328
- }, async ({ machine_id, yes }) => ({
30329
- content: [{ type: "text", text: JSON.stringify(runSync(machine_id, { apply: true, yes }), null, 2) }]
30330
- }));
30765
+ server.tool("machines_setup_preview", "Preview setup actions for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSetupPlan(machine_id), null, 2) }] }));
30766
+ server.tool("machines_setup_apply", "Execute setup actions for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier"), yes: exports_external2.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSetup(machine_id, { apply: true, yes }), null, 2) }] }));
30767
+ server.tool("machines_sync_preview", "Preview sync actions for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildSyncPlan(machine_id), null, 2) }] }));
30768
+ server.tool("machines_sync_apply", "Execute sync actions for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier"), yes: exports_external2.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runSync(machine_id, { apply: true, yes }), null, 2) }] }));
30331
30769
  server.tool("machines_diff", "Show manifest differences between two machines.", {
30332
30770
  left_machine_id: exports_external2.string().describe("Left machine identifier"),
30333
30771
  right_machine_id: exports_external2.string().optional().describe("Right machine identifier")
30334
30772
  }, async ({ left_machine_id, right_machine_id }) => ({
30335
30773
  content: [{ type: "text", text: JSON.stringify(diffMachines(left_machine_id, right_machine_id), null, 2) }]
30336
30774
  }));
30775
+ server.tool("machines_install_claude_status", "Check installed state for Claude, Codex, and Gemini CLIs.", {
30776
+ machine_id: exports_external2.string().optional().describe("Machine identifier"),
30777
+ tools: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to inspect")
30778
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(getClaudeCliStatus(machine_id, tools), null, 2) }] }));
30779
+ server.tool("machines_install_claude_diff", "Show missing and installed Claude, Codex, and Gemini CLIs.", {
30780
+ machine_id: exports_external2.string().optional().describe("Machine identifier"),
30781
+ tools: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to inspect")
30782
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(diffClaudeCli(machine_id, tools), null, 2) }] }));
30337
30783
  server.tool("machines_install_claude_preview", "Preview Claude, Codex, and Gemini CLI install steps for a machine.", {
30338
30784
  machine_id: exports_external2.string().optional().describe("Machine identifier"),
30339
30785
  tools: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install")
30340
- }, async ({ machine_id, tools }) => ({
30341
- content: [{ type: "text", text: JSON.stringify(buildClaudeInstallPlan(machine_id, tools), null, 2) }]
30342
- }));
30786
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(buildClaudeInstallPlan(machine_id, tools), null, 2) }] }));
30343
30787
  server.tool("machines_install_claude_apply", "Execute Claude, Codex, and Gemini CLI install steps for a machine.", {
30344
30788
  machine_id: exports_external2.string().optional().describe("Machine identifier"),
30345
30789
  tools: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install"),
@@ -30347,73 +30791,21 @@ function createMcpServer(version2) {
30347
30791
  }, async ({ machine_id, tools, yes }) => ({
30348
30792
  content: [{ type: "text", text: JSON.stringify(runClaudeInstall(machine_id, tools, { apply: true, yes }), null, 2) }]
30349
30793
  }));
30350
- server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
30351
- content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }]
30352
- }));
30353
- server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", {
30354
- machine_id: exports_external2.string().optional().describe("Machine identifier"),
30355
- yes: exports_external2.boolean().describe("Confirmation flag for execution")
30356
- }, async ({ machine_id, yes }) => ({
30357
- content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }]
30358
- }));
30359
- server.tool("machines_ssh_resolve", "Resolve the best SSH route for a machine.", {
30360
- machine_id: exports_external2.string().describe("Machine identifier"),
30361
- remote_command: exports_external2.string().optional().describe("Optional remote command")
30362
- }, async ({ machine_id, remote_command }) => ({
30363
- content: [
30364
- {
30365
- type: "text",
30366
- text: JSON.stringify({
30367
- resolved: resolveSshTarget(machine_id),
30368
- command: buildSshCommand(machine_id, remote_command)
30369
- }, null, 2)
30370
- }
30371
- ]
30794
+ server.tool("machines_install_tailscale_preview", "Preview Tailscale install steps for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({ content: [{ type: "text", text: JSON.stringify(buildTailscaleInstallPlan(machine_id), null, 2) }] }));
30795
+ server.tool("machines_install_tailscale_apply", "Execute Tailscale install steps for a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier"), yes: exports_external2.boolean().describe("Confirmation flag for execution") }, async ({ machine_id, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runTailscaleInstall(machine_id, { apply: true, yes }), null, 2) }] }));
30796
+ server.tool("machines_ssh_resolve", "Resolve the best SSH route for a machine.", { machine_id: exports_external2.string().describe("Machine identifier"), remote_command: exports_external2.string().optional().describe("Optional remote command") }, async ({ machine_id, remote_command }) => ({
30797
+ content: [{ type: "text", text: JSON.stringify({ resolved: resolveSshTarget(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
30372
30798
  }));
30373
- server.tool("machines_ports", "List listening ports on a machine.", {
30374
- machine_id: exports_external2.string().optional().describe("Machine identifier")
30375
- }, async ({ machine_id }) => ({
30799
+ server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
30376
30800
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
30377
30801
  }));
30378
- server.tool("machines_backup_preview", "Preview backup steps for the current machine.", {
30379
- bucket: exports_external2.string().describe("S3 bucket name"),
30380
- prefix: exports_external2.string().optional().describe("S3 key prefix")
30381
- }, async ({ bucket, prefix }) => ({
30382
- content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }]
30383
- }));
30384
- server.tool("machines_backup_apply", "Execute backup steps for the current machine.", {
30385
- bucket: exports_external2.string().describe("S3 bucket name"),
30386
- prefix: exports_external2.string().optional().describe("S3 key prefix"),
30387
- yes: exports_external2.boolean().describe("Confirmation flag for execution")
30388
- }, async ({ bucket, prefix, yes }) => ({
30389
- content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }]
30390
- }));
30391
- server.tool("machines_cert_preview", "Preview mkcert steps for one or more domains.", {
30392
- domains: exports_external2.array(exports_external2.string()).describe("Domains to issue certificates for")
30393
- }, async ({ domains }) => ({
30394
- content: [{ type: "text", text: JSON.stringify(buildCertPlan(domains), null, 2) }]
30395
- }));
30396
- server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", {
30397
- domains: exports_external2.array(exports_external2.string()).describe("Domains to issue certificates for"),
30398
- yes: exports_external2.boolean().describe("Confirmation flag for execution")
30399
- }, async ({ domains, yes }) => ({
30400
- content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }]
30401
- }));
30402
- server.tool("machines_dns_add", "Add or replace a local domain mapping.", {
30403
- domain: exports_external2.string().describe("Domain name"),
30404
- port: exports_external2.number().describe("Target port"),
30405
- target_host: exports_external2.string().optional().describe("Target host")
30406
- }, async ({ domain, port, target_host }) => ({
30407
- content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, target_host), null, 2) }]
30408
- }));
30409
- server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({
30410
- content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }]
30411
- }));
30412
- server.tool("machines_dns_render", "Render hosts/proxy configuration for a domain.", {
30413
- domain: exports_external2.string().describe("Domain name")
30414
- }, async ({ domain }) => ({
30415
- content: [{ type: "text", text: JSON.stringify(renderDomainMapping(domain), null, 2) }]
30416
- }));
30802
+ server.tool("machines_backup_preview", "Preview backup steps for the current machine.", { bucket: exports_external2.string().describe("S3 bucket name"), prefix: exports_external2.string().optional().describe("S3 key prefix") }, async ({ bucket, prefix }) => ({ content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }] }));
30803
+ server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external2.string().describe("S3 bucket name"), prefix: exports_external2.string().optional().describe("S3 key prefix"), yes: exports_external2.boolean().describe("Confirmation flag for execution") }, async ({ bucket, prefix, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }] }));
30804
+ server.tool("machines_cert_preview", "Preview mkcert steps for one or more domains.", { domains: exports_external2.array(exports_external2.string()).describe("Domains to issue certificates for") }, async ({ domains }) => ({ content: [{ type: "text", text: JSON.stringify(buildCertPlan(domains), null, 2) }] }));
30805
+ server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external2.array(exports_external2.string()).describe("Domains to issue certificates for"), yes: exports_external2.boolean().describe("Confirmation flag for execution") }, async ({ domains, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }] }));
30806
+ server.tool("machines_dns_add", "Add or replace a local domain mapping.", { domain: exports_external2.string().describe("Domain name"), port: exports_external2.number().describe("Target port"), target_host: exports_external2.string().optional().describe("Target host") }, async ({ domain, port, target_host }) => ({ content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, target_host), null, 2) }] }));
30807
+ server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }] }));
30808
+ server.tool("machines_dns_render", "Render hosts/proxy configuration for a domain.", { domain: exports_external2.string().describe("Domain name") }, async ({ domain }) => ({ content: [{ type: "text", text: JSON.stringify(renderDomainMapping(domain), null, 2) }] }));
30417
30809
  server.tool("machines_notifications_add", "Add or replace a notification channel.", {
30418
30810
  channel_id: exports_external2.string().describe("Channel identifier"),
30419
30811
  type: exports_external2.enum(["email", "webhook", "command"]).describe("Notification transport"),
@@ -30421,64 +30813,22 @@ function createMcpServer(version2) {
30421
30813
  events: exports_external2.array(exports_external2.string()).describe("Events routed to this channel"),
30422
30814
  enabled: exports_external2.boolean().optional().describe("Whether the channel is enabled")
30423
30815
  }, async ({ channel_id, type, target, events, enabled }) => ({
30424
- content: [
30425
- {
30426
- type: "text",
30427
- text: JSON.stringify(addNotificationChannel({
30428
- id: channel_id,
30429
- type,
30430
- target,
30431
- events,
30432
- enabled: enabled ?? true
30433
- }), null, 2)
30434
- }
30435
- ]
30816
+ content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events, enabled: enabled ?? true }), null, 2) }]
30436
30817
  }));
30437
30818
  server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
30438
30819
  content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
30439
30820
  }));
30440
- server.tool("machines_notifications_test", "Preview or execute a notification test.", {
30441
- channel_id: exports_external2.string().describe("Channel identifier"),
30442
- event: exports_external2.string().optional().describe("Event name"),
30443
- message: exports_external2.string().optional().describe("Message body"),
30444
- yes: exports_external2.boolean().optional().describe("Execute the test when true")
30445
- }, async ({ channel_id, event, message, yes }) => ({
30446
- content: [
30447
- {
30448
- type: "text",
30449
- text: JSON.stringify(testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes }), null, 2)
30450
- }
30451
- ]
30452
- }));
30453
- server.tool("machines_notifications_remove", "Remove a notification channel.", {
30454
- channel_id: exports_external2.string().describe("Channel identifier")
30455
- }, async ({ channel_id }) => ({
30456
- content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }]
30457
- }));
30458
- server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", {
30459
- host: exports_external2.string().optional().describe("Host interface"),
30460
- port: exports_external2.number().optional().describe("Port number")
30461
- }, async ({ host, port }) => ({
30462
- content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }]
30821
+ server.tool("machines_notifications_test", "Preview or execute a notification test.", { channel_id: exports_external2.string().describe("Channel identifier"), event: exports_external2.string().optional().describe("Event name"), message: exports_external2.string().optional().describe("Message body"), yes: exports_external2.boolean().optional().describe("Execute the test when true") }, async ({ channel_id, event, message, yes }) => ({
30822
+ content: [{ type: "text", text: JSON.stringify(await testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes }), null, 2) }]
30463
30823
  }));
30824
+ server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external2.string().describe("Event name"), message: exports_external2.string().describe("Message body"), channel_id: exports_external2.string().optional().describe("Limit delivery to one channel") }, async ({ event, message, channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id }), null, 2) }] }));
30825
+ server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external2.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] }));
30826
+ server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", { host: exports_external2.string().optional().describe("Host interface"), port: exports_external2.number().optional().describe("Port number") }, async ({ host, port }) => ({ content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }] }));
30464
30827
  server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
30465
30828
  content: [{ type: "text", text: renderDashboardHtml() }]
30466
30829
  }));
30467
30830
  return server;
30468
30831
  }
30469
- // src/version.ts
30470
- import { readFileSync as readFileSync6 } from "fs";
30471
- import { dirname as dirname4, join as join9 } from "path";
30472
- import { fileURLToPath } from "url";
30473
- function getPackageVersion() {
30474
- try {
30475
- const here = dirname4(fileURLToPath(import.meta.url));
30476
- const pkgPath = join9(here, "..", "package.json");
30477
- return JSON.parse(readFileSync6(pkgPath, "utf8")).version || "0.0.0";
30478
- } catch {
30479
- return "0.0.0";
30480
- }
30481
- }
30482
30832
  export {
30483
30833
  writeNotificationConfig,
30484
30834
  writeManifest,
@@ -30491,6 +30841,8 @@ export {
30491
30841
  runTailscaleInstall,
30492
30842
  runSync,
30493
30843
  runSetup,
30844
+ runSelfTest,
30845
+ runDoctor,
30494
30846
  runClaudeInstall,
30495
30847
  runCertPlan,
30496
30848
  runBackup,
@@ -30530,12 +30882,17 @@ export {
30530
30882
  getDbPath,
30531
30883
  getDb,
30532
30884
  getDataDir,
30885
+ getClaudeCliStatus,
30886
+ getAppsStatus,
30533
30887
  getAgentStatus,
30534
30888
  getAdapter,
30535
30889
  fleetSchema,
30536
30890
  ensureParentDir,
30537
30891
  ensureDataDir,
30892
+ dispatchNotificationEvent,
30538
30893
  diffMachines,
30894
+ diffClaudeCli,
30895
+ diffApps,
30539
30896
  detectCurrentMachineManifest,
30540
30897
  createMcpServer,
30541
30898
  countRuns,