@hasna/machines 0.0.2 → 0.0.4

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
@@ -6515,6 +6515,8 @@ var require_dist2 = __commonJS((exports, module) => {
6515
6515
  Object.defineProperty(exports, "__esModule", { value: true });
6516
6516
  exports.default = formatsPlugin;
6517
6517
  });
6518
+ // src/cross-project-types.ts
6519
+ var CROSSREFS_KEY = "_crossRefs";
6518
6520
  // src/paths.ts
6519
6521
  import { existsSync, mkdirSync } from "fs";
6520
6522
  import { dirname, join, resolve } from "path";
@@ -6533,6 +6535,12 @@ function getManifestPath() {
6533
6535
  function getNotificationsPath() {
6534
6536
  return process.env["HASNA_MACHINES_NOTIFICATIONS_PATH"] || join(getDataDir(), "notifications.json");
6535
6537
  }
6538
+ function getClipboardKeyPath() {
6539
+ return process.env["HASNA_MACHINES_CLIPBOARD_KEY_PATH"] || join(getDataDir(), "clipboard.key");
6540
+ }
6541
+ function getClipboardHistoryPath() {
6542
+ return process.env["HASNA_MACHINES_CLIPBOARD_HISTORY_PATH"] || join(getDataDir(), "clipboard-history.json");
6543
+ }
6536
6544
  function ensureParentDir(filePath) {
6537
6545
  if (filePath === ":memory:")
6538
6546
  return;
@@ -20335,17 +20343,95 @@ function runBackup(bucket, prefix = "machines", options = {}) {
20335
20343
  executed
20336
20344
  };
20337
20345
  }
20346
+ // src/remote.ts
20347
+ import { spawnSync as spawnSync2 } from "child_process";
20348
+
20349
+ // src/commands/ssh.ts
20350
+ import { spawnSync } from "child_process";
20351
+ function envReachableHosts() {
20352
+ const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
20353
+ return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
20354
+ }
20355
+ function isReachable(host) {
20356
+ const overrides = envReachableHosts();
20357
+ if (overrides.size > 0) {
20358
+ return overrides.has(host);
20359
+ }
20360
+ const probe = spawnSync("bash", ["-lc", `getent hosts ${host} >/dev/null 2>&1 || ping -c 1 -W 1 ${host} >/dev/null 2>&1`], {
20361
+ stdio: "ignore"
20362
+ });
20363
+ return probe.status === 0;
20364
+ }
20365
+ function resolveSshTarget(machineId) {
20366
+ const machine = getManifestMachine(machineId);
20367
+ if (!machine) {
20368
+ throw new Error(`Machine not found in manifest: ${machineId}`);
20369
+ }
20370
+ const current = detectCurrentMachineManifest();
20371
+ if (machine.id === current.id) {
20372
+ return {
20373
+ machineId,
20374
+ target: "localhost",
20375
+ route: "local"
20376
+ };
20377
+ }
20378
+ const lanTarget = machine.sshAddress || machine.hostname || machine.id;
20379
+ const tailscaleTarget = machine.tailscaleName || machine.hostname || machine.id;
20380
+ const route = isReachable(lanTarget) ? "lan" : "tailscale";
20381
+ return {
20382
+ machineId,
20383
+ target: route === "lan" ? lanTarget : tailscaleTarget,
20384
+ route
20385
+ };
20386
+ }
20387
+ function buildSshCommand(machineId, remoteCommand) {
20388
+ const resolved = resolveSshTarget(machineId);
20389
+ return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
20390
+ }
20391
+
20392
+ // src/remote.ts
20393
+ function runMachineCommand(machineId, command) {
20394
+ const localMachineId = getLocalMachineId();
20395
+ const isLocal = machineId === localMachineId;
20396
+ const route = isLocal ? "local" : resolveSshTarget(machineId).route;
20397
+ const shellCommand = isLocal ? command : buildSshCommand(machineId, command);
20398
+ const result = spawnSync2("bash", ["-lc", shellCommand], {
20399
+ encoding: "utf8",
20400
+ env: process.env
20401
+ });
20402
+ return {
20403
+ machineId,
20404
+ source: route,
20405
+ stdout: result.stdout || "",
20406
+ stderr: result.stderr || "",
20407
+ exitCode: result.status ?? 1
20408
+ };
20409
+ }
20410
+
20338
20411
  // src/commands/apps.ts
20339
20412
  function getPackageName(app) {
20340
20413
  return app.packageName || app.name;
20341
20414
  }
20415
+ function getAppManager(machine, app) {
20416
+ if (app.manager)
20417
+ return app.manager;
20418
+ if (machine.platform === "macos")
20419
+ return "brew";
20420
+ if (machine.platform === "windows")
20421
+ return "winget";
20422
+ return "apt";
20423
+ }
20424
+ function shellQuote(value) {
20425
+ return `'${value.replace(/'/g, `'\\''`)}'`;
20426
+ }
20342
20427
  function buildAppCommand(machine, app) {
20343
20428
  const packageName = getPackageName(app);
20344
- if (app.manager === "custom") {
20429
+ const manager = getAppManager(machine, app);
20430
+ if (manager === "custom") {
20345
20431
  return packageName;
20346
20432
  }
20347
20433
  if (machine.platform === "macos") {
20348
- if (app.manager === "cask") {
20434
+ if (manager === "cask") {
20349
20435
  return `brew install --cask ${packageName}`;
20350
20436
  }
20351
20437
  return `brew install ${packageName}`;
@@ -20355,18 +20441,48 @@ function buildAppCommand(machine, app) {
20355
20441
  }
20356
20442
  return `sudo apt-get install -y ${packageName}`;
20357
20443
  }
20444
+ function buildAppProbeCommand(machine, app) {
20445
+ const packageName = shellQuote(getPackageName(app));
20446
+ const manager = getAppManager(machine, app);
20447
+ if (manager === "custom") {
20448
+ return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
20449
+ }
20450
+ if (machine.platform === "macos") {
20451
+ if (manager === "cask") {
20452
+ return `if brew list --cask ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
20453
+ }
20454
+ 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`;
20455
+ }
20456
+ if (machine.platform === "windows") {
20457
+ return `if winget list --id ${packageName} --exact >/dev/null 2>&1; then printf 'installed=1\\nversion=installed\\n'; else printf 'installed=0\\n'; fi`;
20458
+ }
20459
+ 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`;
20460
+ }
20358
20461
  function buildAppSteps(machine) {
20359
20462
  return (machine.apps || []).map((app) => ({
20360
20463
  id: `app-${app.name}`,
20361
20464
  title: `Install ${app.name} on ${machine.id}`,
20362
20465
  command: buildAppCommand(machine, app),
20363
- manager: app.manager === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
20466
+ manager: getAppManager(machine, app) === "custom" ? "custom" : machine.platform === "macos" ? "brew" : machine.platform === "windows" ? "custom" : "apt",
20364
20467
  privileged: machine.platform === "linux"
20365
20468
  }));
20366
20469
  }
20367
20470
  function resolveMachine(machineId) {
20368
20471
  return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
20369
20472
  }
20473
+ function parseProbeOutput(app, machine, stdout) {
20474
+ const lines = stdout.trim().split(`
20475
+ `).filter(Boolean);
20476
+ const installedLine = lines.find((line) => line.startsWith("installed="));
20477
+ const versionLine = lines.find((line) => line.startsWith("version="));
20478
+ return {
20479
+ name: app.name,
20480
+ packageName: getPackageName(app),
20481
+ manager: getAppManager(machine, app),
20482
+ installed: installedLine === "installed=1",
20483
+ version: versionLine?.slice("version=".length) || undefined
20484
+ };
20485
+ }
20370
20486
  function listApps(machineId) {
20371
20487
  const machine = resolveMachine(machineId);
20372
20488
  return {
@@ -20383,6 +20499,26 @@ function buildAppsPlan(machineId) {
20383
20499
  executed: 0
20384
20500
  };
20385
20501
  }
20502
+ function getAppsStatus(machineId) {
20503
+ const machine = resolveMachine(machineId);
20504
+ const apps = (machine.apps || []).map((app) => {
20505
+ const probe = runMachineCommand(machine.id, buildAppProbeCommand(machine, app));
20506
+ return parseProbeOutput(app, machine, probe.stdout);
20507
+ });
20508
+ return {
20509
+ machineId: machine.id,
20510
+ source: apps.length > 0 ? runMachineCommand(machine.id, "true").source : machine.id === detectCurrentMachineManifest().id ? "local" : runMachineCommand(machine.id, "true").source,
20511
+ apps
20512
+ };
20513
+ }
20514
+ function diffApps(machineId) {
20515
+ const status = getAppsStatus(machineId);
20516
+ return {
20517
+ ...status,
20518
+ missing: status.apps.filter((app) => !app.installed).map((app) => app.name),
20519
+ installed: status.apps.filter((app) => app.installed).map((app) => app.name)
20520
+ };
20521
+ }
20386
20522
  function runAppsInstall(machineId, options = {}) {
20387
20523
  const plan = buildAppsPlan(machineId);
20388
20524
  if (!options.apply)
@@ -20529,6 +20665,58 @@ function renderDomainMapping(domain) {
20529
20665
  keyPath: join8(getDataDir(), "certs", `${entry.domain}-key.pem`)
20530
20666
  };
20531
20667
  }
20668
+ // src/commands/doctor.ts
20669
+ function makeCheck(id, status, summary, detail) {
20670
+ return { id, status, summary, detail };
20671
+ }
20672
+ function parseKeyValueOutput(stdout) {
20673
+ return Object.fromEntries(stdout.trim().split(`
20674
+ `).map((line) => line.split("=")).filter((parts) => parts.length === 2).map(([key, value]) => [key, value]));
20675
+ }
20676
+ function buildDoctorCommand() {
20677
+ return [
20678
+ 'data_dir="${HASNA_MACHINES_DIR:-$HOME/.hasna/machines}"',
20679
+ 'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
20680
+ 'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
20681
+ 'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
20682
+ `printf 'manifest_path=%s\\n' "$manifest_path"`,
20683
+ `printf 'db_path=%s\\n' "$db_path"`,
20684
+ `printf 'notifications_path=%s\\n' "$notifications_path"`,
20685
+ `printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
20686
+ `printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
20687
+ `printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
20688
+ `printf 'bun=%s\\n' "$(bun --version 2>/dev/null || printf missing)"`,
20689
+ `printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
20690
+ `printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
20691
+ `printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
20692
+ `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
20693
+ ].join("; ");
20694
+ }
20695
+ function runDoctor(machineId = getLocalMachineId()) {
20696
+ const manifest = readManifest();
20697
+ const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
20698
+ const details = parseKeyValueOutput(commandChecks.stdout);
20699
+ const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
20700
+ const checks = [
20701
+ makeCheck("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(machineInManifest) : `No manifest entry for ${machineId}`),
20702
+ makeCheck("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${details["manifest_path"] || "unknown"} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`),
20703
+ makeCheck("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${details["db_path"] || "unknown"} ${details["db_exists"] === "yes" ? "exists" : "missing"}`),
20704
+ makeCheck("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${details["notifications_path"] || "unknown"} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`),
20705
+ makeCheck("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
20706
+ makeCheck("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
20707
+ makeCheck("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
20708
+ makeCheck("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
20709
+ makeCheck("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
20710
+ ];
20711
+ return {
20712
+ machineId,
20713
+ source: commandChecks.source,
20714
+ manifestPath: details["manifest_path"],
20715
+ dbPath: details["db_path"],
20716
+ notificationsPath: details["notifications_path"],
20717
+ checks
20718
+ };
20719
+ }
20532
20720
  // src/commands/manifest.ts
20533
20721
  function manifestInit() {
20534
20722
  return writeManifest(getDefaultManifest(), getManifestPath());
@@ -20608,6 +20796,13 @@ var AI_CLI_PACKAGES = {
20608
20796
  codex: "@openai/codex",
20609
20797
  gemini: "@google/gemini-cli"
20610
20798
  };
20799
+ function getToolBinary(tool) {
20800
+ if (tool === "claude")
20801
+ return process.env["HASNA_MACHINES_CLAUDE_BINARY"] || "claude";
20802
+ if (tool === "codex")
20803
+ return process.env["HASNA_MACHINES_CODEX_BINARY"] || "codex";
20804
+ return process.env["HASNA_MACHINES_GEMINI_BINARY"] || "gemini";
20805
+ }
20611
20806
  function normalizeTools(tools) {
20612
20807
  if (!tools || tools.length === 0) {
20613
20808
  return ["claude", "codex", "gemini"];
@@ -20627,8 +20822,27 @@ function buildInstallSteps(machine, tools) {
20627
20822
  manager: "bun"
20628
20823
  }));
20629
20824
  }
20825
+ function resolveMachine2(machineId) {
20826
+ return (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
20827
+ }
20828
+ function buildProbeCommand(tool) {
20829
+ const binary = getToolBinary(tool);
20830
+ 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`;
20831
+ }
20832
+ function parseProbe(tool, stdout) {
20833
+ const lines = stdout.trim().split(`
20834
+ `).filter(Boolean);
20835
+ const installedLine = lines.find((line) => line.startsWith("installed="));
20836
+ const versionLine = lines.find((line) => line.startsWith("version="));
20837
+ return {
20838
+ tool,
20839
+ packageName: AI_CLI_PACKAGES[tool],
20840
+ installed: installedLine === "installed=1",
20841
+ version: versionLine?.slice("version=".length) || undefined
20842
+ };
20843
+ }
20630
20844
  function buildClaudeInstallPlan(machineId, tools) {
20631
- const machine = (machineId ? getManifestMachine(machineId) : null) || detectCurrentMachineManifest();
20845
+ const machine = resolveMachine2(machineId);
20632
20846
  return {
20633
20847
  machineId: machine.id,
20634
20848
  mode: "plan",
@@ -20636,6 +20850,24 @@ function buildClaudeInstallPlan(machineId, tools) {
20636
20850
  executed: 0
20637
20851
  };
20638
20852
  }
20853
+ function getClaudeCliStatus(machineId, tools) {
20854
+ const machine = resolveMachine2(machineId);
20855
+ const normalizedTools = normalizeTools(tools);
20856
+ const route = runMachineCommand(machine.id, "true").source;
20857
+ return {
20858
+ machineId: machine.id,
20859
+ source: route,
20860
+ tools: normalizedTools.map((tool) => parseProbe(tool, runMachineCommand(machine.id, buildProbeCommand(tool)).stdout))
20861
+ };
20862
+ }
20863
+ function diffClaudeCli(machineId, tools) {
20864
+ const status = getClaudeCliStatus(machineId, tools);
20865
+ return {
20866
+ ...status,
20867
+ missing: status.tools.filter((tool) => !tool.installed).map((tool) => tool.tool),
20868
+ installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
20869
+ };
20870
+ }
20639
20871
  function runClaudeInstall(machineId, tools, options = {}) {
20640
20872
  const plan = buildClaudeInstallPlan(machineId, tools);
20641
20873
  if (!options.apply)
@@ -20746,6 +20978,138 @@ var notificationConfigSchema = exports_external2.object({
20746
20978
  function sortChannels(channels) {
20747
20979
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
20748
20980
  }
20981
+ function shellQuote2(value) {
20982
+ return `'${value.replace(/'/g, `'\\''`)}'`;
20983
+ }
20984
+ function hasCommand(binary) {
20985
+ const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], {
20986
+ stdout: "ignore",
20987
+ stderr: "ignore",
20988
+ env: process.env
20989
+ });
20990
+ return result.exitCode === 0;
20991
+ }
20992
+ function buildNotificationPreview(channel, event, message) {
20993
+ if (channel.type === "email") {
20994
+ return `send email to ${channel.target}: [${event}] ${message}`;
20995
+ }
20996
+ if (channel.type === "webhook") {
20997
+ return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
20998
+ }
20999
+ return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
21000
+ }
21001
+ async function dispatchEmail(channel, event, message) {
21002
+ const subject = `[${event}] machines notification`;
21003
+ const body = `To: ${channel.target}
21004
+ Subject: ${subject}
21005
+ Content-Type: text/plain; charset=utf-8
21006
+
21007
+ ${message}
21008
+ `;
21009
+ if (hasCommand("sendmail")) {
21010
+ const result = Bun.spawnSync(["bash", "-lc", "sendmail -t"], {
21011
+ stdin: new TextEncoder().encode(body),
21012
+ stdout: "pipe",
21013
+ stderr: "pipe",
21014
+ env: process.env
21015
+ });
21016
+ if (result.exitCode !== 0) {
21017
+ throw new Error(result.stderr.toString().trim() || `sendmail exited with ${result.exitCode}`);
21018
+ }
21019
+ return {
21020
+ channelId: channel.id,
21021
+ event,
21022
+ delivered: true,
21023
+ transport: channel.type,
21024
+ detail: `Delivered via sendmail to ${channel.target}`
21025
+ };
21026
+ }
21027
+ if (hasCommand("mail")) {
21028
+ const command = `printf %s ${shellQuote2(message)} | mail -s ${shellQuote2(subject)} ${shellQuote2(channel.target)}`;
21029
+ const result = Bun.spawnSync(["bash", "-lc", command], {
21030
+ stdout: "pipe",
21031
+ stderr: "pipe",
21032
+ env: process.env
21033
+ });
21034
+ if (result.exitCode !== 0) {
21035
+ throw new Error(result.stderr.toString().trim() || `mail exited with ${result.exitCode}`);
21036
+ }
21037
+ return {
21038
+ channelId: channel.id,
21039
+ event,
21040
+ delivered: true,
21041
+ transport: channel.type,
21042
+ detail: `Delivered via mail to ${channel.target}`
21043
+ };
21044
+ }
21045
+ throw new Error("No local email transport available. Install sendmail or mail.");
21046
+ }
21047
+ async function dispatchWebhook(channel, event, message) {
21048
+ const response = await fetch(channel.target, {
21049
+ method: "POST",
21050
+ headers: {
21051
+ "content-type": "application/json"
21052
+ },
21053
+ body: JSON.stringify({
21054
+ channelId: channel.id,
21055
+ event,
21056
+ message,
21057
+ sentAt: new Date().toISOString()
21058
+ })
21059
+ });
21060
+ if (!response.ok) {
21061
+ const text = await response.text();
21062
+ throw new Error(`Webhook responded ${response.status}: ${text || response.statusText}`);
21063
+ }
21064
+ return {
21065
+ channelId: channel.id,
21066
+ event,
21067
+ delivered: true,
21068
+ transport: channel.type,
21069
+ detail: `Webhook accepted with HTTP ${response.status}`
21070
+ };
21071
+ }
21072
+ async function dispatchCommand(channel, event, message) {
21073
+ const result = Bun.spawnSync(["bash", "-lc", channel.target], {
21074
+ stdout: "pipe",
21075
+ stderr: "pipe",
21076
+ env: {
21077
+ ...process.env,
21078
+ HASNA_MACHINES_NOTIFICATION_CHANNEL: channel.id,
21079
+ HASNA_MACHINES_NOTIFICATION_EVENT: event,
21080
+ HASNA_MACHINES_NOTIFICATION_MESSAGE: message
21081
+ }
21082
+ });
21083
+ if (result.exitCode !== 0) {
21084
+ throw new Error(result.stderr.toString().trim() || `command exited with ${result.exitCode}`);
21085
+ }
21086
+ const stdout = result.stdout.toString().trim();
21087
+ return {
21088
+ channelId: channel.id,
21089
+ event,
21090
+ delivered: true,
21091
+ transport: channel.type,
21092
+ detail: stdout || "Command completed successfully"
21093
+ };
21094
+ }
21095
+ async function dispatchChannel(channel, event, message) {
21096
+ if (!channel.enabled) {
21097
+ return {
21098
+ channelId: channel.id,
21099
+ event,
21100
+ delivered: false,
21101
+ transport: channel.type,
21102
+ detail: "Channel is disabled"
21103
+ };
21104
+ }
21105
+ if (channel.type === "email") {
21106
+ return dispatchEmail(channel, event, message);
21107
+ }
21108
+ if (channel.type === "webhook") {
21109
+ return dispatchWebhook(channel, event, message);
21110
+ }
21111
+ return dispatchCommand(channel, event, message);
21112
+ }
20749
21113
  function getDefaultNotificationConfig() {
20750
21114
  return {
20751
21115
  version: 1,
@@ -20789,16 +21153,34 @@ function removeNotificationChannel(channelId) {
20789
21153
  channels: config.channels.filter((channel) => channel.id !== channelId)
20790
21154
  });
20791
21155
  }
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}"}`;
21156
+ async function dispatchNotificationEvent(event, message, options = {}) {
21157
+ const channels = readNotificationConfig().channels.filter((channel) => {
21158
+ if (options.channelId && channel.id !== options.channelId) {
21159
+ return false;
21160
+ }
21161
+ return channel.events.includes(event) || event === "manual.test";
21162
+ });
21163
+ const deliveries = [];
21164
+ for (const channel of channels) {
21165
+ try {
21166
+ deliveries.push(await dispatchChannel(channel, event, message));
21167
+ } catch (error) {
21168
+ deliveries.push({
21169
+ channelId: channel.id,
21170
+ event,
21171
+ delivered: false,
21172
+ transport: channel.type,
21173
+ detail: error instanceof Error ? error.message : String(error)
21174
+ });
21175
+ }
20798
21176
  }
20799
- return `${channel.target} --event ${event} --message ${JSON.stringify(message)}`;
21177
+ return {
21178
+ event,
21179
+ message,
21180
+ deliveries
21181
+ };
20800
21182
  }
20801
- function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
21183
+ async function testNotificationChannel(channelId, event = "manual.test", message = "machines notification test", options = {}) {
20802
21184
  const channel = readNotificationConfig().channels.find((entry) => entry.id === channelId);
20803
21185
  if (!channel) {
20804
21186
  throw new Error(`Notification channel not found: ${channelId}`);
@@ -20809,76 +21191,24 @@ function testNotificationChannel(channelId, event = "manual.test", message = "ma
20809
21191
  channelId,
20810
21192
  mode: "plan",
20811
21193
  delivered: false,
20812
- preview
21194
+ preview,
21195
+ detail: "Preview only"
20813
21196
  };
20814
21197
  }
20815
21198
  if (!options.yes) {
20816
21199
  throw new Error("Notification test execution requires --yes.");
20817
21200
  }
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
- }
21201
+ const delivery = await dispatchChannel(channel, event, message);
20828
21202
  return {
20829
21203
  channelId,
20830
21204
  mode: "apply",
20831
- delivered: channel.enabled,
20832
- preview
21205
+ delivered: delivery.delivered,
21206
+ preview,
21207
+ detail: delivery.detail
20833
21208
  };
20834
21209
  }
20835
21210
  // 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
20874
- };
20875
- }
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
- // src/commands/ports.ts
21211
+ import { spawnSync as spawnSync3 } from "child_process";
20882
21212
  function parseSsOutput(output) {
20883
21213
  return output.trim().split(`
20884
21214
  `).map((line) => line.trim()).filter(Boolean).map((line) => {
@@ -20920,7 +21250,7 @@ function listPorts(machineId) {
20920
21250
  const isLocal = targetMachineId === getLocalMachineId();
20921
21251
  const localCommand = "if command -v ss >/dev/null 2>&1; then ss -ltnpH; else lsof -nP -iTCP -sTCP:LISTEN; fi";
20922
21252
  const command = isLocal ? localCommand : buildSshCommand(targetMachineId, localCommand);
20923
- const result = spawnSync2("bash", ["-lc", command], { encoding: "utf8" });
21253
+ const result = spawnSync3("bash", ["-lc", command], { encoding: "utf8" });
20924
21254
  if (result.status !== 0) {
20925
21255
  throw new Error(result.stderr || `Failed to list ports for ${targetMachineId}`);
20926
21256
  }
@@ -20930,6 +21260,24 @@ function listPorts(machineId) {
20930
21260
  listeners: parsePortOutput(result.stdout, format)
20931
21261
  };
20932
21262
  }
21263
+ // src/version.ts
21264
+ import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
21265
+ import { dirname as dirname4, join as join9 } from "path";
21266
+ import { fileURLToPath } from "url";
21267
+ function getPackageVersion() {
21268
+ try {
21269
+ const here = dirname4(fileURLToPath(import.meta.url));
21270
+ const candidates = [join9(here, "..", "package.json"), join9(here, "..", "..", "package.json")];
21271
+ const pkgPath = candidates.find((candidate) => existsSync8(candidate));
21272
+ if (!pkgPath) {
21273
+ return "0.0.0";
21274
+ }
21275
+ return JSON.parse(readFileSync5(pkgPath, "utf8")).version || "0.0.0";
21276
+ } catch {
21277
+ return "0.0.0";
21278
+ }
21279
+ }
21280
+
20933
21281
  // src/commands/status.ts
20934
21282
  function getStatus() {
20935
21283
  const manifest = readManifest();
@@ -20973,13 +21321,27 @@ function getServeInfo(options = {}) {
20973
21321
  host,
20974
21322
  port,
20975
21323
  url: `http://${host}:${port}`,
20976
- routes: ["/", "/health", "/api/status", "/api/manifest", "/api/notifications"]
21324
+ routes: [
21325
+ "/",
21326
+ "/health",
21327
+ "/api/status",
21328
+ "/api/manifest",
21329
+ "/api/notifications",
21330
+ "/api/doctor",
21331
+ "/api/self-test",
21332
+ "/api/apps/status",
21333
+ "/api/apps/diff",
21334
+ "/api/install-claude/status",
21335
+ "/api/install-claude/diff",
21336
+ "/api/notifications/test"
21337
+ ]
20977
21338
  };
20978
21339
  }
20979
21340
  function renderDashboardHtml() {
20980
21341
  const status = getStatus();
20981
21342
  const manifest = manifestList();
20982
21343
  const notifications = listNotificationChannels();
21344
+ const doctor = runDoctor();
20983
21345
  return `<!doctype html>
20984
21346
  <html lang="en">
20985
21347
  <head>
@@ -20995,21 +21357,26 @@ function renderDashboardHtml() {
20995
21357
  .card { background: #121933; border: 1px solid #243057; border-radius: 16px; padding: 20px; }
20996
21358
  .stat { font-size: 32px; font-weight: 700; margin-top: 8px; }
20997
21359
  table { width: 100%; border-collapse: collapse; }
20998
- th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; }
21360
+ th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #243057; vertical-align: top; }
20999
21361
  code { color: #9ed0ff; }
21000
21362
  .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; }
21363
+ .online, .ok { background: #12351f; color: #74f0a7; }
21364
+ .offline, .fail { background: #3b1a1a; color: #ff8c8c; }
21365
+ .unknown, .warn { background: #2f2b16; color: #ffd76a; }
21366
+ ul { margin: 8px 0 0; padding-left: 18px; }
21367
+ .muted { color: #9fb0d9; }
21368
+ .refresh { font-size: 12px; color: #6b7fa3; margin-left: auto; }
21369
+ .updated { transition: opacity 0.3s; }
21004
21370
  </style>
21005
21371
  </head>
21006
21372
  <body>
21007
21373
  <main>
21008
- <h1>Machines Dashboard</h1>
21374
+ <h1>Machines Dashboard <span class="refresh" id="last-updated"></span></h1>
21009
21375
  <div class="grid">
21010
21376
  <section class="card"><div>Manifest machines</div><div class="stat">${status.manifestMachineCount}</div></section>
21011
21377
  <section class="card"><div>Heartbeats</div><div class="stat">${status.heartbeatCount}</div></section>
21012
21378
  <section class="card"><div>Notification channels</div><div class="stat">${notifications.channels.length}</div></section>
21379
+ <section class="card"><div>Doctor warnings</div><div class="stat">${doctor.checks.filter((entry) => entry.status !== "ok").length}</div></section>
21013
21380
  </div>
21014
21381
 
21015
21382
  <section class="card" style="margin-top:16px">
@@ -21027,21 +21394,119 @@ function renderDashboardHtml() {
21027
21394
  </table>
21028
21395
  </section>
21029
21396
 
21397
+ <section class="card" style="margin-top:16px">
21398
+ <h2>Doctor</h2>
21399
+ <table>
21400
+ <thead><tr><th>Check</th><th>Status</th><th>Detail</th></tr></thead>
21401
+ <tbody id="doctor-tbody">
21402
+ ${doctor.checks.map((entry) => `<tr>
21403
+ <td>${escapeHtml(entry.summary)}</td>
21404
+ <td><span class="badge ${escapeHtml(entry.status)}">${escapeHtml(entry.status)}</span></td>
21405
+ <td class="muted">${escapeHtml(entry.detail)}</td>
21406
+ </tr>`).join("")}
21407
+ </tbody>
21408
+ </table>
21409
+ </section>
21410
+
21411
+ <section class="card" style="margin-top:16px">
21412
+ <h2>Apps</h2>
21413
+ <p class="muted">Use <code>/api/apps/status</code> for the full app inventory payload.</p>
21414
+ </section>
21415
+
21416
+ <section class="card" style="margin-top:16px">
21417
+ <h2>AI CLIs</h2>
21418
+ <p class="muted">Use <code>/api/install-claude/status</code> for the full CLI inventory payload.</p>
21419
+ </section>
21420
+
21421
+ <section class="card" style="margin-top:16px">
21422
+ <h2>Self Test</h2>
21423
+ <p class="muted">Use <code>/api/self-test</code> for the full smoke-check payload.</p>
21424
+ </section>
21425
+
21030
21426
  <section class="card" style="margin-top:16px">
21031
21427
  <h2>Manifest</h2>
21032
- <pre>${escapeHtml(JSON.stringify(manifest, null, 2))}</pre>
21428
+ <pre id="manifest-json">${escapeHtml(JSON.stringify(manifest, null, 2))}</pre>
21033
21429
  </section>
21034
21430
  </main>
21431
+ <script>
21432
+ // Auto-refresh dashboard data every 15s
21433
+ const REFRESH_INTERVAL = 15000;
21434
+ async function refreshData() {
21435
+ try {
21436
+ const [statusRes, doctorRes] = await Promise.all([
21437
+ fetch("/api/status"),
21438
+ fetch("/api/doctor"),
21439
+ ]);
21440
+ const status = await statusRes.json();
21441
+ const doctor = await doctorRes.json();
21442
+
21443
+ // Update stat cards
21444
+ const stats = document.querySelectorAll(".stat");
21445
+ if (stats[0]) stats[0].textContent = status.manifestMachineCount;
21446
+ if (stats[1]) stats[1].textContent = status.heartbeatCount;
21447
+
21448
+ // Update machine table
21449
+ const tbody = document.querySelector("tbody");
21450
+ if (tbody && status.machines) {
21451
+ tbody.innerHTML = status.machines
21452
+ .map((m) =>
21453
+ "<tr>" +
21454
+ "<td><code>" + m.machineId + "</code></td>" +
21455
+ "<td>" + (m.platform || "unknown") + "</td>" +
21456
+ '<td><span class="badge ' + m.heartbeatStatus + '">' + m.heartbeatStatus + '</span></td>' +
21457
+ "<td>" + (m.lastHeartbeatAt || "\\u2014") + "</td>" +
21458
+ "</tr>"
21459
+ )
21460
+ .join("");
21461
+ }
21462
+
21463
+ // Update doctor table
21464
+ const doctorTbody = document.getElementById("doctor-tbody");
21465
+ if (doctorTbody && doctor.checks) {
21466
+ doctorTbody.innerHTML = doctor.checks
21467
+ .map((c) =>
21468
+ "<tr>" +
21469
+ "<td>" + c.summary + "</td>" +
21470
+ '<td><span class="badge ' + c.status + '">' + c.status + '</span></td>' +
21471
+ '<td class="muted">' + c.detail + "</td>" +
21472
+ "</tr>"
21473
+ )
21474
+ .join("");
21475
+ }
21476
+
21477
+ // Update timestamp
21478
+ document.getElementById("last-updated").textContent =
21479
+ "updated " + new Date().toLocaleTimeString();
21480
+ } catch (e) {
21481
+ // Silently ignore fetch errors during page unload
21482
+ }
21483
+ }
21484
+ document.getElementById("last-updated").textContent =
21485
+ "updated " + new Date().toLocaleTimeString();
21486
+ setInterval(refreshData, REFRESH_INTERVAL);
21487
+ </script>
21035
21488
  </body>
21036
21489
  </html>`;
21037
21490
  }
21491
+ async function parseJsonBody(request) {
21492
+ try {
21493
+ return await request.json();
21494
+ } catch {
21495
+ return {};
21496
+ }
21497
+ }
21498
+ function jsonError(message, status = 400) {
21499
+ return Response.json({ error: message }, { status });
21500
+ }
21038
21501
  function startDashboardServer(options = {}) {
21039
21502
  const info = getServeInfo(options);
21040
21503
  return Bun.serve({
21041
21504
  hostname: info.host,
21042
21505
  port: info.port,
21043
- fetch(request) {
21506
+ async fetch(request) {
21044
21507
  const url = new URL(request.url);
21508
+ const machineId = url.searchParams.get("machine") || undefined;
21509
+ const tools = url.searchParams.get("tools")?.split(",").map((value) => value.trim()).filter(Boolean);
21045
21510
  if (url.pathname === "/health") {
21046
21511
  return Response.json({ ok: true, ...getServeInfo(options) });
21047
21512
  }
@@ -21054,6 +21519,43 @@ function startDashboardServer(options = {}) {
21054
21519
  if (url.pathname === "/api/notifications") {
21055
21520
  return Response.json(listNotificationChannels());
21056
21521
  }
21522
+ if (url.pathname === "/api/doctor") {
21523
+ return Response.json(runDoctor(machineId));
21524
+ }
21525
+ if (url.pathname === "/api/self-test") {
21526
+ return Response.json(runSelfTest());
21527
+ }
21528
+ if (url.pathname === "/api/apps/status") {
21529
+ return Response.json(getAppsStatus(machineId));
21530
+ }
21531
+ if (url.pathname === "/api/apps/diff") {
21532
+ return Response.json(diffApps(machineId));
21533
+ }
21534
+ if (url.pathname === "/api/install-claude/status") {
21535
+ return Response.json(getClaudeCliStatus(machineId, tools));
21536
+ }
21537
+ if (url.pathname === "/api/install-claude/diff") {
21538
+ return Response.json(diffClaudeCli(machineId, tools));
21539
+ }
21540
+ if (url.pathname === "/api/notifications/test") {
21541
+ if (request.method !== "POST") {
21542
+ return jsonError("Use POST for notification tests.", 405);
21543
+ }
21544
+ const body = await parseJsonBody(request);
21545
+ const channelId = typeof body["channelId"] === "string" ? body["channelId"] : undefined;
21546
+ if (!channelId) {
21547
+ return jsonError("channelId is required.");
21548
+ }
21549
+ const event = typeof body["event"] === "string" ? body["event"] : undefined;
21550
+ const message = typeof body["message"] === "string" ? body["message"] : undefined;
21551
+ const apply = body["apply"] === true;
21552
+ const yes = body["yes"] === true;
21553
+ try {
21554
+ return Response.json(await testNotificationChannel(channelId, event, message, { apply, yes }));
21555
+ } catch (error) {
21556
+ return jsonError(error instanceof Error ? error.message : String(error));
21557
+ }
21558
+ }
21057
21559
  return new Response(renderDashboardHtml(), {
21058
21560
  headers: {
21059
21561
  "content-type": "text/html; charset=utf-8"
@@ -21062,6 +21564,36 @@ function startDashboardServer(options = {}) {
21062
21564
  }
21063
21565
  });
21064
21566
  }
21567
+
21568
+ // src/commands/self-test.ts
21569
+ function check(id, status, summary, detail) {
21570
+ return { id, status, summary, detail };
21571
+ }
21572
+ function runSelfTest() {
21573
+ const version = getPackageVersion();
21574
+ const status = getStatus();
21575
+ const doctor = runDoctor();
21576
+ const serveInfo = getServeInfo();
21577
+ const html = renderDashboardHtml();
21578
+ const notifications = listNotificationChannels();
21579
+ const apps = listApps(status.machineId);
21580
+ const appsDiff = diffApps(status.machineId);
21581
+ const cliPlan = buildClaudeInstallPlan(status.machineId);
21582
+ return {
21583
+ machineId: getLocalMachineId(),
21584
+ checks: [
21585
+ check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
21586
+ check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
21587
+ check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
21588
+ check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
21589
+ check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
21590
+ check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
21591
+ check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
21592
+ check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
21593
+ check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
21594
+ ]
21595
+ };
21596
+ }
21065
21597
  // src/commands/setup.ts
21066
21598
  function quote3(value) {
21067
21599
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -21177,7 +21709,7 @@ function runSetup(machineId, options = {}) {
21177
21709
  return summary;
21178
21710
  }
21179
21711
  // src/commands/sync.ts
21180
- import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync as copyFileSync2 } from "fs";
21712
+ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync6, symlinkSync, copyFileSync as copyFileSync2 } from "fs";
21181
21713
  function quote4(value) {
21182
21714
  return `'${value.replace(/'/g, `'\\''`)}'`;
21183
21715
  }
@@ -21208,12 +21740,12 @@ function packageInstallCommand(machine, packageName, manager = machine.platform
21208
21740
  function detectPackageActions(machine) {
21209
21741
  return (machine.packages || []).map((pkg, index) => {
21210
21742
  const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
21211
- const check = Bun.spawnSync(["bash", "-lc", packageCheckCommand(machine, pkg.name, manager)], {
21743
+ const check2 = Bun.spawnSync(["bash", "-lc", packageCheckCommand(machine, pkg.name, manager)], {
21212
21744
  stdout: "ignore",
21213
21745
  stderr: "ignore",
21214
21746
  env: process.env
21215
21747
  });
21216
- const installed = check.exitCode === 0;
21748
+ const installed = check2.exitCode === 0;
21217
21749
  return {
21218
21750
  id: `package-${index + 1}`,
21219
21751
  title: `${installed ? "Package present" : "Install package"} ${pkg.name}`,
@@ -21225,15 +21757,15 @@ function detectPackageActions(machine) {
21225
21757
  }
21226
21758
  function detectFileActions(machine) {
21227
21759
  return (machine.files || []).map((file, index) => {
21228
- const sourceExists = existsSync8(file.source);
21229
- const targetExists = existsSync8(file.target);
21760
+ const sourceExists = existsSync9(file.source);
21761
+ const targetExists = existsSync9(file.target);
21230
21762
  let status = "missing";
21231
21763
  if (sourceExists && targetExists) {
21232
21764
  if (file.mode === "symlink") {
21233
21765
  status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
21234
21766
  } else {
21235
- const source = readFileSync5(file.source, "utf8");
21236
- const target = readFileSync5(file.target, "utf8");
21767
+ const source = readFileSync6(file.source, "utf8");
21768
+ const target = readFileSync6(file.target, "utf8");
21237
21769
  status = source === target ? "ok" : "drifted";
21238
21770
  }
21239
21771
  }
@@ -25384,7 +25916,7 @@ var ZodType3 = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
25384
25916
  inst.parseAsync = async (data, params) => parseAsync2(inst, data, params, { callee: inst.parseAsync });
25385
25917
  inst.safeParseAsync = async (data, params) => safeParseAsync3(inst, data, params);
25386
25918
  inst.spa = inst.safeParseAsync;
25387
- inst.refine = (check, params) => inst.check(refine(check, params));
25919
+ inst.refine = (check2, params) => inst.check(refine(check2, params));
25388
25920
  inst.superRefine = (refinement) => inst.check(superRefine(refinement));
25389
25921
  inst.overwrite = (fn) => inst.check(_overwrite(fn));
25390
25922
  inst.optional = () => optional(inst);
@@ -25928,7 +26460,7 @@ var ZodCustom = /* @__PURE__ */ $constructor("ZodCustom", (inst, def) => {
25928
26460
  $ZodCustom.init(inst, def);
25929
26461
  ZodType3.init(inst, def);
25930
26462
  });
25931
- function check(fn) {
26463
+ function check2(fn) {
25932
26464
  const ch = new $ZodCheck({
25933
26465
  check: "custom"
25934
26466
  });
@@ -25942,7 +26474,7 @@ function refine(fn, _params = {}) {
25942
26474
  return _refine(ZodCustom, fn, _params);
25943
26475
  }
25944
26476
  function superRefine(fn) {
25945
- const ch = check((payload) => {
26477
+ const ch = check2((payload) => {
25946
26478
  payload.addIssue = (issue2) => {
25947
26479
  if (typeof issue2 === "string") {
25948
26480
  payload.issues.push(exports_util.issue(issue2, payload.value, ch._zod.def));
@@ -26941,38 +27473,38 @@ function parseBigintDef(def, refs) {
26941
27473
  };
26942
27474
  if (!def.checks)
26943
27475
  return res;
26944
- for (const check2 of def.checks) {
26945
- switch (check2.kind) {
27476
+ for (const check3 of def.checks) {
27477
+ switch (check3.kind) {
26946
27478
  case "min":
26947
27479
  if (refs.target === "jsonSchema7") {
26948
- if (check2.inclusive) {
26949
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
27480
+ if (check3.inclusive) {
27481
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
26950
27482
  } else {
26951
- setResponseValueAndErrors(res, "exclusiveMinimum", check2.value, check2.message, refs);
27483
+ setResponseValueAndErrors(res, "exclusiveMinimum", check3.value, check3.message, refs);
26952
27484
  }
26953
27485
  } else {
26954
- if (!check2.inclusive) {
27486
+ if (!check3.inclusive) {
26955
27487
  res.exclusiveMinimum = true;
26956
27488
  }
26957
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
27489
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
26958
27490
  }
26959
27491
  break;
26960
27492
  case "max":
26961
27493
  if (refs.target === "jsonSchema7") {
26962
- if (check2.inclusive) {
26963
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
27494
+ if (check3.inclusive) {
27495
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
26964
27496
  } else {
26965
- setResponseValueAndErrors(res, "exclusiveMaximum", check2.value, check2.message, refs);
27497
+ setResponseValueAndErrors(res, "exclusiveMaximum", check3.value, check3.message, refs);
26966
27498
  }
26967
27499
  } else {
26968
- if (!check2.inclusive) {
27500
+ if (!check3.inclusive) {
26969
27501
  res.exclusiveMaximum = true;
26970
27502
  }
26971
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
27503
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
26972
27504
  }
26973
27505
  break;
26974
27506
  case "multipleOf":
26975
- setResponseValueAndErrors(res, "multipleOf", check2.value, check2.message, refs);
27507
+ setResponseValueAndErrors(res, "multipleOf", check3.value, check3.message, refs);
26976
27508
  break;
26977
27509
  }
26978
27510
  }
@@ -27028,13 +27560,13 @@ var integerDateParser = (def, refs) => {
27028
27560
  if (refs.target === "openApi3") {
27029
27561
  return res;
27030
27562
  }
27031
- for (const check2 of def.checks) {
27032
- switch (check2.kind) {
27563
+ for (const check3 of def.checks) {
27564
+ switch (check3.kind) {
27033
27565
  case "min":
27034
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
27566
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
27035
27567
  break;
27036
27568
  case "max":
27037
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
27569
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
27038
27570
  break;
27039
27571
  }
27040
27572
  }
@@ -27152,125 +27684,125 @@ function parseStringDef(def, refs) {
27152
27684
  type: "string"
27153
27685
  };
27154
27686
  if (def.checks) {
27155
- for (const check2 of def.checks) {
27156
- switch (check2.kind) {
27687
+ for (const check3 of def.checks) {
27688
+ switch (check3.kind) {
27157
27689
  case "min":
27158
- setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check2.value) : check2.value, check2.message, refs);
27690
+ setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check3.value) : check3.value, check3.message, refs);
27159
27691
  break;
27160
27692
  case "max":
27161
- setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check2.value) : check2.value, check2.message, refs);
27693
+ setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check3.value) : check3.value, check3.message, refs);
27162
27694
  break;
27163
27695
  case "email":
27164
27696
  switch (refs.emailStrategy) {
27165
27697
  case "format:email":
27166
- addFormat(res, "email", check2.message, refs);
27698
+ addFormat(res, "email", check3.message, refs);
27167
27699
  break;
27168
27700
  case "format:idn-email":
27169
- addFormat(res, "idn-email", check2.message, refs);
27701
+ addFormat(res, "idn-email", check3.message, refs);
27170
27702
  break;
27171
27703
  case "pattern:zod":
27172
- addPattern(res, zodPatterns.email, check2.message, refs);
27704
+ addPattern(res, zodPatterns.email, check3.message, refs);
27173
27705
  break;
27174
27706
  }
27175
27707
  break;
27176
27708
  case "url":
27177
- addFormat(res, "uri", check2.message, refs);
27709
+ addFormat(res, "uri", check3.message, refs);
27178
27710
  break;
27179
27711
  case "uuid":
27180
- addFormat(res, "uuid", check2.message, refs);
27712
+ addFormat(res, "uuid", check3.message, refs);
27181
27713
  break;
27182
27714
  case "regex":
27183
- addPattern(res, check2.regex, check2.message, refs);
27715
+ addPattern(res, check3.regex, check3.message, refs);
27184
27716
  break;
27185
27717
  case "cuid":
27186
- addPattern(res, zodPatterns.cuid, check2.message, refs);
27718
+ addPattern(res, zodPatterns.cuid, check3.message, refs);
27187
27719
  break;
27188
27720
  case "cuid2":
27189
- addPattern(res, zodPatterns.cuid2, check2.message, refs);
27721
+ addPattern(res, zodPatterns.cuid2, check3.message, refs);
27190
27722
  break;
27191
27723
  case "startsWith":
27192
- addPattern(res, RegExp(`^${escapeLiteralCheckValue(check2.value, refs)}`), check2.message, refs);
27724
+ addPattern(res, RegExp(`^${escapeLiteralCheckValue(check3.value, refs)}`), check3.message, refs);
27193
27725
  break;
27194
27726
  case "endsWith":
27195
- addPattern(res, RegExp(`${escapeLiteralCheckValue(check2.value, refs)}$`), check2.message, refs);
27727
+ addPattern(res, RegExp(`${escapeLiteralCheckValue(check3.value, refs)}$`), check3.message, refs);
27196
27728
  break;
27197
27729
  case "datetime":
27198
- addFormat(res, "date-time", check2.message, refs);
27730
+ addFormat(res, "date-time", check3.message, refs);
27199
27731
  break;
27200
27732
  case "date":
27201
- addFormat(res, "date", check2.message, refs);
27733
+ addFormat(res, "date", check3.message, refs);
27202
27734
  break;
27203
27735
  case "time":
27204
- addFormat(res, "time", check2.message, refs);
27736
+ addFormat(res, "time", check3.message, refs);
27205
27737
  break;
27206
27738
  case "duration":
27207
- addFormat(res, "duration", check2.message, refs);
27739
+ addFormat(res, "duration", check3.message, refs);
27208
27740
  break;
27209
27741
  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);
27742
+ setResponseValueAndErrors(res, "minLength", typeof res.minLength === "number" ? Math.max(res.minLength, check3.value) : check3.value, check3.message, refs);
27743
+ setResponseValueAndErrors(res, "maxLength", typeof res.maxLength === "number" ? Math.min(res.maxLength, check3.value) : check3.value, check3.message, refs);
27212
27744
  break;
27213
27745
  case "includes": {
27214
- addPattern(res, RegExp(escapeLiteralCheckValue(check2.value, refs)), check2.message, refs);
27746
+ addPattern(res, RegExp(escapeLiteralCheckValue(check3.value, refs)), check3.message, refs);
27215
27747
  break;
27216
27748
  }
27217
27749
  case "ip": {
27218
- if (check2.version !== "v6") {
27219
- addFormat(res, "ipv4", check2.message, refs);
27750
+ if (check3.version !== "v6") {
27751
+ addFormat(res, "ipv4", check3.message, refs);
27220
27752
  }
27221
- if (check2.version !== "v4") {
27222
- addFormat(res, "ipv6", check2.message, refs);
27753
+ if (check3.version !== "v4") {
27754
+ addFormat(res, "ipv6", check3.message, refs);
27223
27755
  }
27224
27756
  break;
27225
27757
  }
27226
27758
  case "base64url":
27227
- addPattern(res, zodPatterns.base64url, check2.message, refs);
27759
+ addPattern(res, zodPatterns.base64url, check3.message, refs);
27228
27760
  break;
27229
27761
  case "jwt":
27230
- addPattern(res, zodPatterns.jwt, check2.message, refs);
27762
+ addPattern(res, zodPatterns.jwt, check3.message, refs);
27231
27763
  break;
27232
27764
  case "cidr": {
27233
- if (check2.version !== "v6") {
27234
- addPattern(res, zodPatterns.ipv4Cidr, check2.message, refs);
27765
+ if (check3.version !== "v6") {
27766
+ addPattern(res, zodPatterns.ipv4Cidr, check3.message, refs);
27235
27767
  }
27236
- if (check2.version !== "v4") {
27237
- addPattern(res, zodPatterns.ipv6Cidr, check2.message, refs);
27768
+ if (check3.version !== "v4") {
27769
+ addPattern(res, zodPatterns.ipv6Cidr, check3.message, refs);
27238
27770
  }
27239
27771
  break;
27240
27772
  }
27241
27773
  case "emoji":
27242
- addPattern(res, zodPatterns.emoji(), check2.message, refs);
27774
+ addPattern(res, zodPatterns.emoji(), check3.message, refs);
27243
27775
  break;
27244
27776
  case "ulid": {
27245
- addPattern(res, zodPatterns.ulid, check2.message, refs);
27777
+ addPattern(res, zodPatterns.ulid, check3.message, refs);
27246
27778
  break;
27247
27779
  }
27248
27780
  case "base64": {
27249
27781
  switch (refs.base64Strategy) {
27250
27782
  case "format:binary": {
27251
- addFormat(res, "binary", check2.message, refs);
27783
+ addFormat(res, "binary", check3.message, refs);
27252
27784
  break;
27253
27785
  }
27254
27786
  case "contentEncoding:base64": {
27255
- setResponseValueAndErrors(res, "contentEncoding", "base64", check2.message, refs);
27787
+ setResponseValueAndErrors(res, "contentEncoding", "base64", check3.message, refs);
27256
27788
  break;
27257
27789
  }
27258
27790
  case "pattern:zod": {
27259
- addPattern(res, zodPatterns.base64, check2.message, refs);
27791
+ addPattern(res, zodPatterns.base64, check3.message, refs);
27260
27792
  break;
27261
27793
  }
27262
27794
  }
27263
27795
  break;
27264
27796
  }
27265
27797
  case "nanoid": {
27266
- addPattern(res, zodPatterns.nanoid, check2.message, refs);
27798
+ addPattern(res, zodPatterns.nanoid, check3.message, refs);
27267
27799
  }
27268
27800
  case "toLowerCase":
27269
27801
  case "toUpperCase":
27270
27802
  case "trim":
27271
27803
  break;
27272
27804
  default:
27273
- ((_) => {})(check2);
27805
+ ((_) => {})(check3);
27274
27806
  }
27275
27807
  }
27276
27808
  }
@@ -27639,42 +28171,42 @@ function parseNumberDef(def, refs) {
27639
28171
  };
27640
28172
  if (!def.checks)
27641
28173
  return res;
27642
- for (const check2 of def.checks) {
27643
- switch (check2.kind) {
28174
+ for (const check3 of def.checks) {
28175
+ switch (check3.kind) {
27644
28176
  case "int":
27645
28177
  res.type = "integer";
27646
- addErrorMessage(res, "type", check2.message, refs);
28178
+ addErrorMessage(res, "type", check3.message, refs);
27647
28179
  break;
27648
28180
  case "min":
27649
28181
  if (refs.target === "jsonSchema7") {
27650
- if (check2.inclusive) {
27651
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
28182
+ if (check3.inclusive) {
28183
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
27652
28184
  } else {
27653
- setResponseValueAndErrors(res, "exclusiveMinimum", check2.value, check2.message, refs);
28185
+ setResponseValueAndErrors(res, "exclusiveMinimum", check3.value, check3.message, refs);
27654
28186
  }
27655
28187
  } else {
27656
- if (!check2.inclusive) {
28188
+ if (!check3.inclusive) {
27657
28189
  res.exclusiveMinimum = true;
27658
28190
  }
27659
- setResponseValueAndErrors(res, "minimum", check2.value, check2.message, refs);
28191
+ setResponseValueAndErrors(res, "minimum", check3.value, check3.message, refs);
27660
28192
  }
27661
28193
  break;
27662
28194
  case "max":
27663
28195
  if (refs.target === "jsonSchema7") {
27664
- if (check2.inclusive) {
27665
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
28196
+ if (check3.inclusive) {
28197
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
27666
28198
  } else {
27667
- setResponseValueAndErrors(res, "exclusiveMaximum", check2.value, check2.message, refs);
28199
+ setResponseValueAndErrors(res, "exclusiveMaximum", check3.value, check3.message, refs);
27668
28200
  }
27669
28201
  } else {
27670
- if (!check2.inclusive) {
28202
+ if (!check3.inclusive) {
27671
28203
  res.exclusiveMaximum = true;
27672
28204
  }
27673
- setResponseValueAndErrors(res, "maximum", check2.value, check2.message, refs);
28205
+ setResponseValueAndErrors(res, "maximum", check3.value, check3.message, refs);
27674
28206
  }
27675
28207
  break;
27676
28208
  case "multipleOf":
27677
- setResponseValueAndErrors(res, "multipleOf", check2.value, check2.message, refs);
28209
+ setResponseValueAndErrors(res, "multipleOf", check3.value, check3.message, refs);
27678
28210
  break;
27679
28211
  }
27680
28212
  }
@@ -30238,7 +30770,11 @@ var EMPTY_COMPLETION_RESULT = {
30238
30770
  // src/mcp/server.ts
30239
30771
  var MACHINE_MCP_TOOL_NAMES = [
30240
30772
  "machines_status",
30773
+ "machines_doctor",
30774
+ "machines_self_test",
30241
30775
  "machines_apps_list",
30776
+ "machines_apps_status",
30777
+ "machines_apps_diff",
30242
30778
  "machines_apps_plan",
30243
30779
  "machines_apps_apply",
30244
30780
  "machines_manifest",
@@ -30254,6 +30790,8 @@ var MACHINE_MCP_TOOL_NAMES = [
30254
30790
  "machines_diff",
30255
30791
  "machines_install_tailscale_preview",
30256
30792
  "machines_install_tailscale_apply",
30793
+ "machines_install_claude_status",
30794
+ "machines_install_claude_diff",
30257
30795
  "machines_install_claude_preview",
30258
30796
  "machines_install_claude_apply",
30259
30797
  "machines_ssh_resolve",
@@ -30268,30 +30806,25 @@ var MACHINE_MCP_TOOL_NAMES = [
30268
30806
  "machines_notifications_add",
30269
30807
  "machines_notifications_list",
30270
30808
  "machines_notifications_test",
30809
+ "machines_notifications_dispatch",
30271
30810
  "machines_notifications_remove",
30272
30811
  "machines_serve_info",
30273
30812
  "machines_serve_dashboard"
30274
30813
  ];
30275
30814
  function createMcpServer(version2) {
30276
- const server = new McpServer({
30277
- name: "machines",
30278
- version: version2
30279
- });
30815
+ const server = new McpServer({ name: "machines", version: version2 });
30280
30816
  server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
30281
30817
  content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
30282
30818
  }));
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) }]
30819
+ 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) }] }));
30820
+ server.tool("machines_self_test", "Run local package smoke checks.", {}, async () => ({
30821
+ content: [{ type: "text", text: JSON.stringify(runSelfTest(), null, 2) }]
30294
30822
  }));
30823
+ 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) }] }));
30824
+ 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) }] }));
30825
+ 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) }] }));
30826
+ 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) }] }));
30827
+ 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
30828
  server.tool("machines_manifest", "Read the current fleet manifest.", {}, async () => ({
30296
30829
  content: [{ type: "text", text: JSON.stringify(manifestList(), null, 2) }]
30297
30830
  }));
@@ -30301,45 +30834,33 @@ function createMcpServer(version2) {
30301
30834
  server.tool("machines_manifest_bootstrap", "Detect and upsert the current machine into the fleet manifest.", {}, async () => ({
30302
30835
  content: [{ type: "text", text: JSON.stringify(manifestBootstrapCurrentMachine(), null, 2) }]
30303
30836
  }));
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
- }));
30837
+ 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) }] }));
30838
+ 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
30839
  server.tool("machines_agent_status", "List current machine agent heartbeats.", {}, async () => ({
30311
30840
  content: [{ type: "text", text: JSON.stringify(getAgentStatus(), null, 2) }]
30312
30841
  }));
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
- }));
30842
+ 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) }] }));
30843
+ 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) }] }));
30844
+ 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) }] }));
30845
+ 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
30846
  server.tool("machines_diff", "Show manifest differences between two machines.", {
30332
30847
  left_machine_id: exports_external2.string().describe("Left machine identifier"),
30333
30848
  right_machine_id: exports_external2.string().optional().describe("Right machine identifier")
30334
30849
  }, async ({ left_machine_id, right_machine_id }) => ({
30335
30850
  content: [{ type: "text", text: JSON.stringify(diffMachines(left_machine_id, right_machine_id), null, 2) }]
30336
30851
  }));
30852
+ server.tool("machines_install_claude_status", "Check installed state for Claude, Codex, and Gemini CLIs.", {
30853
+ machine_id: exports_external2.string().optional().describe("Machine identifier"),
30854
+ tools: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to inspect")
30855
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(getClaudeCliStatus(machine_id, tools), null, 2) }] }));
30856
+ server.tool("machines_install_claude_diff", "Show missing and installed Claude, Codex, and Gemini CLIs.", {
30857
+ machine_id: exports_external2.string().optional().describe("Machine identifier"),
30858
+ tools: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to inspect")
30859
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(diffClaudeCli(machine_id, tools), null, 2) }] }));
30337
30860
  server.tool("machines_install_claude_preview", "Preview Claude, Codex, and Gemini CLI install steps for a machine.", {
30338
30861
  machine_id: exports_external2.string().optional().describe("Machine identifier"),
30339
30862
  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
- }));
30863
+ }, async ({ machine_id, tools }) => ({ content: [{ type: "text", text: JSON.stringify(buildClaudeInstallPlan(machine_id, tools), null, 2) }] }));
30343
30864
  server.tool("machines_install_claude_apply", "Execute Claude, Codex, and Gemini CLI install steps for a machine.", {
30344
30865
  machine_id: exports_external2.string().optional().describe("Machine identifier"),
30345
30866
  tools: exports_external2.array(exports_external2.enum(["claude", "codex", "gemini"])).optional().describe("AI CLIs to install"),
@@ -30347,73 +30868,21 @@ function createMcpServer(version2) {
30347
30868
  }, async ({ machine_id, tools, yes }) => ({
30348
30869
  content: [{ type: "text", text: JSON.stringify(runClaudeInstall(machine_id, tools, { apply: true, yes }), null, 2) }]
30349
30870
  }));
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
- ]
30871
+ 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) }] }));
30872
+ 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) }] }));
30873
+ 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 }) => ({
30874
+ content: [{ type: "text", text: JSON.stringify({ resolved: resolveSshTarget(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
30372
30875
  }));
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 }) => ({
30876
+ server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external2.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
30376
30877
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
30377
30878
  }));
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
- }));
30879
+ 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) }] }));
30880
+ 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) }] }));
30881
+ 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) }] }));
30882
+ 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) }] }));
30883
+ 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) }] }));
30884
+ server.tool("machines_dns_list", "List local domain mappings.", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(listDomainMappings(), null, 2) }] }));
30885
+ 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
30886
  server.tool("machines_notifications_add", "Add or replace a notification channel.", {
30418
30887
  channel_id: exports_external2.string().describe("Channel identifier"),
30419
30888
  type: exports_external2.enum(["email", "webhook", "command"]).describe("Notification transport"),
@@ -30421,68 +30890,22 @@ function createMcpServer(version2) {
30421
30890
  events: exports_external2.array(exports_external2.string()).describe("Events routed to this channel"),
30422
30891
  enabled: exports_external2.boolean().optional().describe("Whether the channel is enabled")
30423
30892
  }, 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
- ]
30893
+ content: [{ type: "text", text: JSON.stringify(addNotificationChannel({ id: channel_id, type, target, events, enabled: enabled ?? true }), null, 2) }]
30436
30894
  }));
30437
30895
  server.tool("machines_notifications_list", "List notification channels.", {}, async () => ({
30438
30896
  content: [{ type: "text", text: JSON.stringify(listNotificationChannels(), null, 2) }]
30439
30897
  }));
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) }]
30898
+ 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 }) => ({
30899
+ content: [{ type: "text", text: JSON.stringify(await testNotificationChannel(channel_id, event, message, { apply: Boolean(yes), yes }), null, 2) }]
30463
30900
  }));
30901
+ 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) }] }));
30902
+ 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) }] }));
30903
+ 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
30904
  server.tool("machines_serve_dashboard", "Render the current dashboard HTML.", {}, async () => ({
30465
30905
  content: [{ type: "text", text: renderDashboardHtml() }]
30466
30906
  }));
30467
30907
  return server;
30468
30908
  }
30469
- // src/version.ts
30470
- import { existsSync as existsSync9, 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 candidates = [join9(here, "..", "package.json"), join9(here, "..", "..", "package.json")];
30477
- const pkgPath = candidates.find((candidate) => existsSync9(candidate));
30478
- if (!pkgPath) {
30479
- return "0.0.0";
30480
- }
30481
- return JSON.parse(readFileSync6(pkgPath, "utf8")).version || "0.0.0";
30482
- } catch {
30483
- return "0.0.0";
30484
- }
30485
- }
30486
30909
  export {
30487
30910
  writeNotificationConfig,
30488
30911
  writeManifest,
@@ -30495,6 +30918,8 @@ export {
30495
30918
  runTailscaleInstall,
30496
30919
  runSync,
30497
30920
  runSetup,
30921
+ runSelfTest,
30922
+ runDoctor,
30498
30923
  runClaudeInstall,
30499
30924
  runCertPlan,
30500
30925
  runBackup,
@@ -30534,12 +30959,19 @@ export {
30534
30959
  getDbPath,
30535
30960
  getDb,
30536
30961
  getDataDir,
30962
+ getClipboardKeyPath,
30963
+ getClipboardHistoryPath,
30964
+ getClaudeCliStatus,
30965
+ getAppsStatus,
30537
30966
  getAgentStatus,
30538
30967
  getAdapter,
30539
30968
  fleetSchema,
30540
30969
  ensureParentDir,
30541
30970
  ensureDataDir,
30971
+ dispatchNotificationEvent,
30542
30972
  diffMachines,
30973
+ diffClaudeCli,
30974
+ diffApps,
30543
30975
  detectCurrentMachineManifest,
30544
30976
  createMcpServer,
30545
30977
  countRuns,
@@ -30553,5 +30985,6 @@ export {
30553
30985
  buildAppsPlan,
30554
30986
  addNotificationChannel,
30555
30987
  addDomainMapping,
30556
- MACHINE_MCP_TOOL_NAMES
30988
+ MACHINE_MCP_TOOL_NAMES,
30989
+ CROSSREFS_KEY
30557
30990
  };