@agenticmail/core 0.4.0 → 0.5.1

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.d.ts CHANGED
@@ -1424,14 +1424,15 @@ declare class DependencyInstaller {
1424
1424
  */
1425
1425
  installDocker(): Promise<void>;
1426
1426
  /**
1427
- * Attempt to start the Docker daemon.
1428
- * On macOS: opens Docker Desktop app.
1429
- * On Linux: tries systemctl.
1427
+ * Attempt to start the Docker daemon using multiple strategies.
1428
+ * On macOS: tries Docker Desktop app, then docker CLI commands.
1429
+ * On Linux: tries systemctl, then dockerd direct, then snap.
1430
1430
  */
1431
1431
  private startDockerDaemon;
1432
1432
  /**
1433
- * Wait for Docker daemon to be ready (up to 3 minutes).
1434
- * Docker Desktop can take 1-2+ minutes on first launch.
1433
+ * Wait for Docker daemon to be ready, with automatic retry strategies.
1434
+ * Tries multiple approaches to start Docker if the first one fails.
1435
+ * Reports progress as a percentage (0-100).
1435
1436
  */
1436
1437
  private waitForDocker;
1437
1438
  /**
@@ -1449,6 +1450,92 @@ declare class DependencyInstaller {
1449
1450
  installAll(composePath: string): Promise<void>;
1450
1451
  }
1451
1452
 
1453
+ interface ServiceStatus {
1454
+ installed: boolean;
1455
+ running: boolean;
1456
+ platform: 'launchd' | 'systemd' | 'unsupported';
1457
+ servicePath: string | null;
1458
+ }
1459
+ /**
1460
+ * ServiceManager handles auto-start on boot for the AgenticMail API server.
1461
+ * - macOS: LaunchAgent plist (user-level, no sudo needed)
1462
+ * - Linux: systemd user service (user-level, no sudo needed)
1463
+ */
1464
+ declare class ServiceManager {
1465
+ private os;
1466
+ /**
1467
+ * Get the path to the service file.
1468
+ */
1469
+ private getServicePath;
1470
+ /**
1471
+ * Find the Node.js binary path.
1472
+ */
1473
+ private getNodePath;
1474
+ /**
1475
+ * Find the API server entry point.
1476
+ * Searches common locations where agenticmail is installed.
1477
+ */
1478
+ private getApiEntryPath;
1479
+ /**
1480
+ * Cache the API entry path so the service can find it later.
1481
+ */
1482
+ cacheApiEntryPath(entryPath: string): void;
1483
+ /**
1484
+ * Get the current package version.
1485
+ */
1486
+ private getVersion;
1487
+ /**
1488
+ * Generate a wrapper script that waits for Docker before starting the API.
1489
+ * This ensures AgenticMail doesn't fail on boot when Docker is still loading.
1490
+ */
1491
+ private generateStartScript;
1492
+ /**
1493
+ * Generate the launchd plist content for macOS.
1494
+ * More robust than OpenClaw's plist:
1495
+ * - Wrapper script waits for Docker + Stalwart before starting
1496
+ * - KeepAlive: true (unconditional — always restart, not just on crash)
1497
+ * - SoftResourceLimits for file descriptors (email servers need many)
1498
+ * - StartInterval as backup heartbeat (checks every 5 min)
1499
+ * - Service version tracking in env vars
1500
+ */
1501
+ private generatePlist;
1502
+ /**
1503
+ * Generate the systemd user service content for Linux.
1504
+ * More robust than basic services:
1505
+ * - Wrapper script waits for Docker + Stalwart
1506
+ * - Restart=always (unconditional)
1507
+ * - WatchdogSec for health monitoring
1508
+ * - File descriptor limits
1509
+ * - Proper dependency ordering
1510
+ */
1511
+ private generateSystemdUnit;
1512
+ /**
1513
+ * Install the auto-start service.
1514
+ */
1515
+ install(): {
1516
+ installed: boolean;
1517
+ message: string;
1518
+ };
1519
+ /**
1520
+ * Uninstall the auto-start service.
1521
+ */
1522
+ uninstall(): {
1523
+ removed: boolean;
1524
+ message: string;
1525
+ };
1526
+ /**
1527
+ * Get the current service status.
1528
+ */
1529
+ status(): ServiceStatus;
1530
+ /**
1531
+ * Reinstall the service (useful after config changes or updates).
1532
+ */
1533
+ reinstall(): {
1534
+ installed: boolean;
1535
+ message: string;
1536
+ };
1537
+ }
1538
+
1452
1539
  interface SetupConfig {
1453
1540
  masterKey: string;
1454
1541
  stalwart: {
@@ -1523,4 +1610,4 @@ declare class SetupManager {
1523
1610
  isInitialized(): boolean;
1524
1611
  }
1525
1612
 
1526
- export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DNSConfigurator, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, type PurchasedDomain, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, type TunnelConfig, TunnelManager, WARNING_THRESHOLD, type WatcherOptions, buildInboundSecurityAdvisory, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, getDatabase, isInternalEmail, isValidPhoneNumber, normalizePhoneNumber, parseEmail, parseGoogleVoiceSms, resolveConfig, sanitizeEmail, saveConfig, scanOutboundEmail, scoreEmail, startRelayBridge };
1613
+ export { AGENT_ROLES, AccountManager, type AddressInfo, type Agent, AgentDeletionService, type AgentRole, AgenticMailClient, type AgenticMailClientOptions, type AgenticMailConfig, type ArchiveAndDeleteOptions, type ArchivedEmail, type Attachment, type AttachmentAdvisory, CloudflareClient, type CreateAgentOptions, DEFAULT_AGENT_NAME, DEFAULT_AGENT_ROLE, DNSConfigurator, type DeletionReport, type DeletionSummary, DependencyChecker, DependencyInstaller, type DependencyStatus, type DnsRecord, type DnsSetupResult, type DomainInfo, DomainManager, type DomainModeConfig, type DomainPurchaseResult, DomainPurchaser, type DomainSearchResult, type DomainSetupResult, type EmailEnvelope, EmailSearchIndex, type FolderInfo, type GatewayConfig, GatewayManager, type GatewayManagerOptions, type GatewayMode, type GatewayStatus, type InboundEmail, type InboxEvent, type InboxExpungeEvent, type InboxFlagsEvent, type InboxNewEvent, InboxWatcher, type InboxWatcherOptions, type InstallProgress, type LinkAdvisory, type LocalSmtpConfig, MailReceiver, type MailReceiverOptions, MailSender, type MailSenderOptions, type MailboxInfo, type OutboundCategory, type OutboundScanInput, type OutboundScanResult, type OutboundWarning, type ParsedAttachment, type ParsedEmail, type ParsedSms, type PurchasedDomain, RELAY_PRESETS, RelayBridge, type RelayBridgeOptions, type RelayConfig, RelayGateway, type RelayProvider, type RelaySearchResult, SPAM_THRESHOLD, type SanitizeDetection, type SanitizeResult, type SearchCriteria, type SearchableEmail, type SecurityAdvisory, type SendMailOptions, type SendResult, type SendResultWithRaw, ServiceManager, type ServiceStatus, type SetupConfig, SetupManager, type SetupResult, type Severity, type SmsConfig, SmsManager, type SmsMessage, SmsPoller, type SpamCategory, type SpamResult, type SpamRuleMatch, StalwartAdmin, type StalwartAdminOptions, type StalwartPrincipal, type TunnelConfig, TunnelManager, WARNING_THRESHOLD, type WatcherOptions, buildInboundSecurityAdvisory, closeDatabase, createTestDatabase, debug, debugWarn, ensureDataDir, extractVerificationCode, getDatabase, isInternalEmail, isValidPhoneNumber, normalizePhoneNumber, parseEmail, parseGoogleVoiceSms, resolveConfig, sanitizeEmail, saveConfig, scanOutboundEmail, scoreEmail, startRelayBridge };
package/dist/index.js CHANGED
@@ -818,10 +818,10 @@ var StalwartAdmin = class {
818
818
  return ["exec", "agenticmail-stalwart", "stalwart-cli", "-u", "http://localhost:8080", "-c", creds];
819
819
  }
820
820
  async updateSetting(key, value) {
821
- const { execFileSync: execFileSync3 } = await import("child_process");
821
+ const { execFileSync: execFileSync4 } = await import("child_process");
822
822
  const cli = this.cliArgs();
823
823
  try {
824
- execFileSync3(
824
+ execFileSync4(
825
825
  "docker",
826
826
  [...cli, "server", "delete-config", key],
827
827
  { timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] }
@@ -829,13 +829,13 @@ var StalwartAdmin = class {
829
829
  } catch {
830
830
  }
831
831
  try {
832
- execFileSync3(
832
+ execFileSync4(
833
833
  "docker",
834
834
  [...cli, "server", "add-config", key, value],
835
835
  { timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] }
836
836
  );
837
837
  } catch {
838
- const output = execFileSync3(
838
+ const output = execFileSync4(
839
839
  "docker",
840
840
  [...cli, "server", "list-config", key],
841
841
  { timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] }
@@ -850,14 +850,14 @@ var StalwartAdmin = class {
850
850
  * Critical for email deliverability — must match the sending domain.
851
851
  */
852
852
  async setHostname(domain) {
853
- const { readFileSync: readFileSync3, writeFileSync: writeFileSync4 } = await import("fs");
854
- const { homedir: homedir7 } = await import("os");
855
- const { join: join8 } = await import("path");
856
- const configPath = join8(homedir7(), ".agenticmail", "stalwart.toml");
853
+ const { readFileSync: readFileSync4, writeFileSync: writeFileSync5 } = await import("fs");
854
+ const { homedir: homedir8 } = await import("os");
855
+ const { join: join9 } = await import("path");
856
+ const configPath = join9(homedir8(), ".agenticmail", "stalwart.toml");
857
857
  try {
858
- let config = readFileSync3(configPath, "utf-8");
858
+ let config = readFileSync4(configPath, "utf-8");
859
859
  config = config.replace(/^hostname\s*=\s*"[^"]*"/m, `hostname = "${domain}"`);
860
- writeFileSync4(configPath, config);
860
+ writeFileSync5(configPath, config);
861
861
  console.log(`[Stalwart] Updated hostname to "${domain}" in stalwart.toml`);
862
862
  } catch (err) {
863
863
  throw new Error(`Failed to set config server.hostname=${domain}`);
@@ -866,15 +866,15 @@ var StalwartAdmin = class {
866
866
  // --- DKIM ---
867
867
  /** Path to the host-side stalwart.toml (mounted read-only into container) */
868
868
  get configPath() {
869
- const { homedir: homedir7 } = __require("os");
870
- const { join: join8 } = __require("path");
871
- return join8(homedir7(), ".agenticmail", "stalwart.toml");
869
+ const { homedir: homedir8 } = __require("os");
870
+ const { join: join9 } = __require("path");
871
+ return join9(homedir8(), ".agenticmail", "stalwart.toml");
872
872
  }
873
873
  /** Path to host-side DKIM key directory */
874
874
  get dkimDir() {
875
- const { homedir: homedir7 } = __require("os");
876
- const { join: join8 } = __require("path");
877
- return join8(homedir7(), ".agenticmail");
875
+ const { homedir: homedir8 } = __require("os");
876
+ const { join: join9 } = __require("path");
877
+ return join9(homedir8(), ".agenticmail");
878
878
  }
879
879
  /**
880
880
  * Create/reuse a DKIM signing key for a domain.
@@ -882,7 +882,7 @@ var StalwartAdmin = class {
882
882
  * Returns the public key (base64, no headers) for DNS TXT record.
883
883
  */
884
884
  async createDkimSignature(domain, selector = "agenticmail") {
885
- const { execFileSync: execFileSync3 } = await import("child_process");
885
+ const { execFileSync: execFileSync4 } = await import("child_process");
886
886
  const signatureId = `agenticmail-${domain.replace(/\./g, "-")}`;
887
887
  const cli = this.cliArgs();
888
888
  const existing = await this.getSettings(`signature.${signatureId}`);
@@ -890,7 +890,7 @@ var StalwartAdmin = class {
890
890
  console.log(`[DKIM] Reusing existing signature "${signatureId}" from Stalwart DB`);
891
891
  } else {
892
892
  try {
893
- execFileSync3("docker", [...cli, "server", "delete-config", `signature.${signatureId}`], {
893
+ execFileSync4("docker", [...cli, "server", "delete-config", `signature.${signatureId}`], {
894
894
  timeout: 1e4,
895
895
  stdio: ["ignore", "pipe", "pipe"]
896
896
  });
@@ -898,7 +898,7 @@ var StalwartAdmin = class {
898
898
  }
899
899
  console.log(`[DKIM] Creating RSA signature for ${domain} via stalwart-cli`);
900
900
  try {
901
- execFileSync3("docker", [...cli, "dkim", "create", "rsa", domain, signatureId, selector], {
901
+ execFileSync4("docker", [...cli, "dkim", "create", "rsa", domain, signatureId, selector], {
902
902
  timeout: 15e3,
903
903
  stdio: ["ignore", "pipe", "pipe"]
904
904
  });
@@ -915,7 +915,7 @@ var StalwartAdmin = class {
915
915
  ["auth.dkim.sign.0001.else", "false"]
916
916
  ];
917
917
  for (const [key, value] of rules) {
918
- execFileSync3("docker", [...cli, "server", "add-config", key, value], {
918
+ execFileSync4("docker", [...cli, "server", "add-config", key, value], {
919
919
  timeout: 1e4,
920
920
  stdio: ["ignore", "pipe", "pipe"]
921
921
  });
@@ -923,7 +923,7 @@ var StalwartAdmin = class {
923
923
  }
924
924
  let publicKey;
925
925
  try {
926
- const output = execFileSync3("docker", [...cli, "dkim", "get-public-key", signatureId], {
926
+ const output = execFileSync4("docker", [...cli, "dkim", "get-public-key", signatureId], {
927
927
  timeout: 1e4,
928
928
  stdio: ["ignore", "pipe", "pipe"]
929
929
  }).toString();
@@ -934,7 +934,7 @@ var StalwartAdmin = class {
934
934
  throw new Error(`Failed to get DKIM public key: ${err.message}`);
935
935
  }
936
936
  try {
937
- execFileSync3("docker", [...cli, "server", "reload-config"], {
937
+ execFileSync4("docker", [...cli, "server", "reload-config"], {
938
938
  timeout: 1e4,
939
939
  stdio: ["ignore", "pipe", "pipe"]
940
940
  });
@@ -947,9 +947,9 @@ var StalwartAdmin = class {
947
947
  * Restart the Stalwart Docker container and wait for it to be ready.
948
948
  */
949
949
  async restartContainer() {
950
- const { execFileSync: execFileSync3 } = await import("child_process");
950
+ const { execFileSync: execFileSync4 } = await import("child_process");
951
951
  try {
952
- execFileSync3("docker", ["restart", "agenticmail-stalwart"], { timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
952
+ execFileSync4("docker", ["restart", "agenticmail-stalwart"], { timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] });
953
953
  for (let i = 0; i < 15; i++) {
954
954
  try {
955
955
  const res = await fetch(`${this.baseUrl}/health`, { signal: AbortSignal.timeout(2e3) });
@@ -975,12 +975,12 @@ var StalwartAdmin = class {
975
975
  * This bypasses the need for a PTR record on the sending IP.
976
976
  */
977
977
  async configureOutboundRelay(config) {
978
- const { readFileSync: readFileSync3, writeFileSync: writeFileSync4 } = await import("fs");
979
- const { homedir: homedir7 } = await import("os");
980
- const { join: join8 } = await import("path");
978
+ const { readFileSync: readFileSync4, writeFileSync: writeFileSync5 } = await import("fs");
979
+ const { homedir: homedir8 } = await import("os");
980
+ const { join: join9 } = await import("path");
981
981
  const routeName = config.routeName ?? "gmail";
982
- const tomlPath = join8(homedir7(), ".agenticmail", "stalwart.toml");
983
- let toml = readFileSync3(tomlPath, "utf-8");
982
+ const tomlPath = join9(homedir8(), ".agenticmail", "stalwart.toml");
983
+ let toml = readFileSync4(tomlPath, "utf-8");
984
984
  toml = toml.replace(/\n\[queue\.route\.gmail\][\s\S]*?(?=\n\[|$)/, "");
985
985
  toml = toml.replace(/\n\[queue\.strategy\][\s\S]*?(?=\n\[|$)/, "");
986
986
  toml += `
@@ -999,7 +999,7 @@ auth.secret = "${config.password}"
999
999
  route = [ { if = "is_local_domain('', rcpt_domain)", then = "'local'" },
1000
1000
  { else = "'${routeName}'" } ]
1001
1001
  `;
1002
- writeFileSync4(tomlPath, toml, "utf-8");
1002
+ writeFileSync5(tomlPath, toml, "utf-8");
1003
1003
  await this.restartContainer();
1004
1004
  }
1005
1005
  };
@@ -3819,8 +3819,8 @@ var CloudflareClient = class {
3819
3819
  let available = false;
3820
3820
  if (result.supported_tld && !hasRegistration) {
3821
3821
  try {
3822
- const { execFileSync: execFileSync3 } = await import("child_process");
3823
- const whoisOutput = execFileSync3("whois", [domain], { timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] }).toString().toLowerCase();
3822
+ const { execFileSync: execFileSync4 } = await import("child_process");
3823
+ const whoisOutput = execFileSync4("whois", [domain], { timeout: 1e4, stdio: ["ignore", "pipe", "pipe"] }).toString().toLowerCase();
3824
3824
  available = whoisOutput.includes("domain not found") || whoisOutput.includes("no match") || whoisOutput.includes("not found") || whoisOutput.includes("no data found") || whoisOutput.includes("status: free") || whoisOutput.includes("no entries found");
3825
3825
  } catch {
3826
3826
  available = false;
@@ -4284,8 +4284,8 @@ var TunnelManager = class {
4284
4284
  return this.binPath;
4285
4285
  }
4286
4286
  try {
4287
- const { execFileSync: execFileSync3 } = await import("child_process");
4288
- const sysPath = execFileSync3("which", ["cloudflared"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
4287
+ const { execFileSync: execFileSync4 } = await import("child_process");
4288
+ const sysPath = execFileSync4("which", ["cloudflared"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
4289
4289
  if (sysPath && existsSync2(sysPath)) {
4290
4290
  this.binPath = sysPath;
4291
4291
  return sysPath;
@@ -5177,12 +5177,12 @@ var GatewayManager = class {
5177
5177
  zone = await this.cfClient.createZone(domain);
5178
5178
  }
5179
5179
  const existingRecords = await this.cfClient.listDnsRecords(zone.id);
5180
- const { homedir: homedir7 } = await import("os");
5181
- const backupDir = join4(homedir7(), ".agenticmail");
5180
+ const { homedir: homedir8 } = await import("os");
5181
+ const backupDir = join4(homedir8(), ".agenticmail");
5182
5182
  const backupPath = join4(backupDir, `dns-backup-${domain}-${Date.now()}.json`);
5183
- const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync4 } = await import("fs");
5184
- mkdirSync4(backupDir, { recursive: true });
5185
- writeFileSync4(backupPath, JSON.stringify({
5183
+ const { writeFileSync: writeFileSync5, mkdirSync: mkdirSync5 } = await import("fs");
5184
+ mkdirSync5(backupDir, { recursive: true });
5185
+ writeFileSync5(backupPath, JSON.stringify({
5186
5186
  domain,
5187
5187
  zoneId: zone.id,
5188
5188
  backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5813,9 +5813,9 @@ var RELAY_PRESETS = {
5813
5813
 
5814
5814
  // src/setup/index.ts
5815
5815
  import { randomBytes as randomBytes2 } from "crypto";
5816
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, chmodSync } from "fs";
5817
- import { join as join7 } from "path";
5818
- import { homedir as homedir6 } from "os";
5816
+ import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, chmodSync } from "fs";
5817
+ import { join as join8 } from "path";
5818
+ import { homedir as homedir7 } from "os";
5819
5819
 
5820
5820
  // src/setup/deps.ts
5821
5821
  import { execFileSync } from "child_process";
@@ -5893,11 +5893,129 @@ var DependencyChecker = class {
5893
5893
  };
5894
5894
 
5895
5895
  // src/setup/installer.ts
5896
- import { execFileSync as execFileSync2, execSync } from "child_process";
5896
+ import { execFileSync as execFileSync2, execSync, spawn as spawnChild } from "child_process";
5897
5897
  import { existsSync as existsSync4 } from "fs";
5898
5898
  import { writeFile, rename, chmod as chmod2, mkdir as mkdir2, unlink } from "fs/promises";
5899
5899
  import { join as join6 } from "path";
5900
5900
  import { homedir as homedir5, platform as platform2, arch as arch2 } from "os";
5901
+ function runWithRollingOutput(command, args, opts = {}) {
5902
+ const maxLines = opts.maxLines ?? 20;
5903
+ const timeout = opts.timeout ?? 3e5;
5904
+ return new Promise((resolve, reject) => {
5905
+ const child = spawnChild(command, args, {
5906
+ stdio: ["ignore", "pipe", "pipe"],
5907
+ timeout
5908
+ });
5909
+ const lines = [];
5910
+ let displayedCount = 0;
5911
+ let fullOutput = "";
5912
+ const processData = (data) => {
5913
+ const text = data.toString();
5914
+ fullOutput += text;
5915
+ const newLines = text.split("\n");
5916
+ for (const line of newLines) {
5917
+ const trimmed = line.trimEnd();
5918
+ if (!trimmed) continue;
5919
+ lines.push(trimmed);
5920
+ if (displayedCount > 0) {
5921
+ const toClear = Math.min(displayedCount, maxLines);
5922
+ process.stdout.write(`\x1B[${toClear}A`);
5923
+ for (let i = 0; i < toClear; i++) {
5924
+ process.stdout.write("\x1B[2K\n");
5925
+ }
5926
+ process.stdout.write(`\x1B[${toClear}A`);
5927
+ }
5928
+ const visible = lines.slice(-maxLines);
5929
+ for (const vLine of visible) {
5930
+ process.stdout.write(` \x1B[90m${vLine.slice(0, 100)}\x1B[0m
5931
+ `);
5932
+ }
5933
+ displayedCount = visible.length;
5934
+ }
5935
+ };
5936
+ child.stdout?.on("data", processData);
5937
+ child.stderr?.on("data", processData);
5938
+ child.on("close", (code) => {
5939
+ if (displayedCount > 0) {
5940
+ process.stdout.write(`\x1B[${displayedCount}A`);
5941
+ for (let i = 0; i < displayedCount; i++) {
5942
+ process.stdout.write("\x1B[2K\n");
5943
+ }
5944
+ process.stdout.write(`\x1B[${displayedCount}A`);
5945
+ }
5946
+ resolve({ exitCode: code ?? 1, fullOutput });
5947
+ });
5948
+ child.on("error", (err) => {
5949
+ if (displayedCount > 0) {
5950
+ process.stdout.write(`\x1B[${displayedCount}A`);
5951
+ for (let i = 0; i < displayedCount; i++) {
5952
+ process.stdout.write("\x1B[2K\n");
5953
+ }
5954
+ process.stdout.write(`\x1B[${displayedCount}A`);
5955
+ }
5956
+ reject(err);
5957
+ });
5958
+ });
5959
+ }
5960
+ function runShellWithRollingOutput(cmd, opts = {}) {
5961
+ const maxLines = opts.maxLines ?? 20;
5962
+ const timeout = opts.timeout ?? 3e5;
5963
+ return new Promise((resolve, reject) => {
5964
+ const child = spawnChild("sh", ["-c", cmd], {
5965
+ stdio: ["ignore", "pipe", "pipe"],
5966
+ timeout
5967
+ });
5968
+ const lines = [];
5969
+ let displayedCount = 0;
5970
+ let fullOutput = "";
5971
+ const processData = (data) => {
5972
+ const text = data.toString();
5973
+ fullOutput += text;
5974
+ const newLines = text.split("\n");
5975
+ for (const line of newLines) {
5976
+ const trimmed = line.trimEnd();
5977
+ if (!trimmed) continue;
5978
+ lines.push(trimmed);
5979
+ if (displayedCount > 0) {
5980
+ const toClear = Math.min(displayedCount, maxLines);
5981
+ process.stdout.write(`\x1B[${toClear}A`);
5982
+ for (let i = 0; i < toClear; i++) {
5983
+ process.stdout.write("\x1B[2K\n");
5984
+ }
5985
+ process.stdout.write(`\x1B[${toClear}A`);
5986
+ }
5987
+ const visible = lines.slice(-maxLines);
5988
+ for (const vLine of visible) {
5989
+ process.stdout.write(` \x1B[90m${vLine.slice(0, 100)}\x1B[0m
5990
+ `);
5991
+ }
5992
+ displayedCount = visible.length;
5993
+ }
5994
+ };
5995
+ child.stdout?.on("data", processData);
5996
+ child.stderr?.on("data", processData);
5997
+ child.on("close", (code) => {
5998
+ if (displayedCount > 0) {
5999
+ process.stdout.write(`\x1B[${displayedCount}A`);
6000
+ for (let i = 0; i < displayedCount; i++) {
6001
+ process.stdout.write("\x1B[2K\n");
6002
+ }
6003
+ process.stdout.write(`\x1B[${displayedCount}A`);
6004
+ }
6005
+ resolve({ exitCode: code ?? 1, fullOutput });
6006
+ });
6007
+ child.on("error", (err) => {
6008
+ if (displayedCount > 0) {
6009
+ process.stdout.write(`\x1B[${displayedCount}A`);
6010
+ for (let i = 0; i < displayedCount; i++) {
6011
+ process.stdout.write("\x1B[2K\n");
6012
+ }
6013
+ process.stdout.write(`\x1B[${displayedCount}A`);
6014
+ }
6015
+ reject(err);
6016
+ });
6017
+ });
6018
+ }
5901
6019
  var DependencyInstaller = class {
5902
6020
  onProgress;
5903
6021
  constructor(onProgress) {
@@ -5935,14 +6053,20 @@ var DependencyInstaller = class {
5935
6053
  } catch {
5936
6054
  throw new Error("Homebrew is required to install Docker on macOS. Install it from https://brew.sh then try again.");
5937
6055
  }
5938
- execFileSync2("brew", ["install", "--cask", "docker"], { timeout: 3e5, stdio: "inherit" });
6056
+ const brewResult = await runWithRollingOutput("brew", ["install", "--cask", "docker"], { timeout: 3e5 });
6057
+ if (brewResult.exitCode !== 0) {
6058
+ throw new Error("Failed to install Docker via Homebrew. Try: brew install --cask docker");
6059
+ }
5939
6060
  this.onProgress("Docker installed. Starting Docker Desktop...");
5940
6061
  this.startDockerDaemon();
5941
6062
  await this.waitForDocker();
5942
6063
  } else if (os === "linux") {
5943
6064
  this.onProgress("Installing Docker...");
5944
6065
  try {
5945
- execSync("curl -fsSL https://get.docker.com | sh", { timeout: 3e5, stdio: "inherit" });
6066
+ const result = await runShellWithRollingOutput("curl -fsSL https://get.docker.com | sh", { timeout: 3e5 });
6067
+ if (result.exitCode !== 0) {
6068
+ throw new Error("Install script failed");
6069
+ }
5946
6070
  } catch {
5947
6071
  throw new Error("Failed to install Docker. Install it manually: https://docs.docker.com/get-docker/");
5948
6072
  }
@@ -5952,48 +6076,123 @@ var DependencyInstaller = class {
5952
6076
  }
5953
6077
  }
5954
6078
  /**
5955
- * Attempt to start the Docker daemon.
5956
- * On macOS: opens Docker Desktop app.
5957
- * On Linux: tries systemctl.
6079
+ * Attempt to start the Docker daemon using multiple strategies.
6080
+ * On macOS: tries Docker Desktop app, then docker CLI commands.
6081
+ * On Linux: tries systemctl, then dockerd direct, then snap.
5958
6082
  */
5959
- startDockerDaemon() {
6083
+ startDockerDaemon(strategy) {
5960
6084
  const os = platform2();
5961
6085
  if (os === "darwin") {
5962
- try {
5963
- execFileSync2("open", ["-a", "Docker"], { timeout: 1e4, stdio: "ignore" });
5964
- } catch {
6086
+ switch (strategy) {
6087
+ case "cli":
6088
+ try {
6089
+ execSync("docker context use default 2>/dev/null; docker info", { timeout: 5e3, stdio: "ignore" });
6090
+ } catch {
6091
+ }
6092
+ break;
6093
+ case "reopen":
6094
+ try {
6095
+ execSync(`osascript -e 'quit app "Docker"'`, { timeout: 5e3, stdio: "ignore" });
6096
+ } catch {
6097
+ }
6098
+ try {
6099
+ execFileSync2("sleep", ["2"], { timeout: 5e3, stdio: "ignore" });
6100
+ } catch {
6101
+ }
6102
+ try {
6103
+ execFileSync2("open", ["-a", "Docker"], { timeout: 1e4, stdio: "ignore" });
6104
+ } catch {
6105
+ }
6106
+ break;
6107
+ case "background":
6108
+ try {
6109
+ const appBin = "/Applications/Docker.app/Contents/MacOS/Docker";
6110
+ if (existsSync4(appBin)) {
6111
+ execSync(`"${appBin}" &`, { timeout: 5e3, stdio: "ignore", shell: "sh" });
6112
+ }
6113
+ } catch {
6114
+ }
6115
+ break;
6116
+ default:
6117
+ try {
6118
+ execFileSync2("open", ["-a", "Docker"], { timeout: 1e4, stdio: "ignore" });
6119
+ } catch {
6120
+ }
5965
6121
  }
5966
6122
  } else if (os === "linux") {
5967
- try {
5968
- execFileSync2("sudo", ["systemctl", "start", "docker"], { timeout: 15e3, stdio: "ignore" });
5969
- } catch {
6123
+ switch (strategy) {
6124
+ case "snap":
6125
+ try {
6126
+ execFileSync2("sudo", ["snap", "start", "docker"], { timeout: 15e3, stdio: "ignore" });
6127
+ } catch {
6128
+ }
6129
+ break;
6130
+ case "service":
6131
+ try {
6132
+ execFileSync2("sudo", ["service", "docker", "start"], { timeout: 15e3, stdio: "ignore" });
6133
+ } catch {
6134
+ }
6135
+ break;
6136
+ default:
6137
+ try {
6138
+ execFileSync2("sudo", ["systemctl", "start", "docker"], { timeout: 15e3, stdio: "ignore" });
6139
+ } catch {
6140
+ }
5970
6141
  }
5971
6142
  }
5972
6143
  }
5973
6144
  /**
5974
- * Wait for Docker daemon to be ready (up to 3 minutes).
5975
- * Docker Desktop can take 1-2+ minutes on first launch.
6145
+ * Wait for Docker daemon to be ready, with automatic retry strategies.
6146
+ * Tries multiple approaches to start Docker if the first one fails.
6147
+ * Reports progress as a percentage (0-100).
5976
6148
  */
5977
6149
  async waitForDocker() {
5978
- this.onProgress("Waiting for Docker to start (this can take a minute)...");
5979
- const maxWait = 18e4;
6150
+ const os = platform2();
6151
+ const strategies = os === "darwin" ? ["default", "cli", "reopen", "background"] : ["default", "service", "snap"];
6152
+ const totalTime = 24e4;
6153
+ const perStrategyTime = Math.floor(totalTime / strategies.length);
5980
6154
  const start = Date.now();
5981
- let attempts = 0;
5982
- while (Date.now() - start < maxWait) {
6155
+ let strategyIdx = 0;
6156
+ this.onProgress("__progress__:0:Starting Docker...");
6157
+ while (Date.now() - start < totalTime) {
5983
6158
  try {
5984
6159
  execFileSync2("docker", ["info"], { timeout: 5e3, stdio: "ignore" });
6160
+ this.onProgress("__progress__:100:Docker is ready!");
5985
6161
  return;
5986
6162
  } catch {
5987
6163
  }
5988
- attempts++;
5989
- if (attempts % 5 === 0) {
5990
- const elapsed = Math.round((Date.now() - start) / 1e3);
5991
- this.onProgress(`Still waiting for Docker to start (${elapsed}s)...`);
6164
+ const elapsed = Date.now() - start;
6165
+ const pct = Math.min(95, Math.round(elapsed / totalTime * 100));
6166
+ const currentStrategyElapsed = elapsed - strategyIdx * perStrategyTime;
6167
+ if (currentStrategyElapsed >= perStrategyTime && strategyIdx < strategies.length - 1) {
6168
+ strategyIdx++;
6169
+ const strategy = strategies[strategyIdx];
6170
+ const msgs = {
6171
+ cli: "Trying Docker CLI...",
6172
+ reopen: "Restarting Docker Desktop...",
6173
+ background: "Trying direct launch...",
6174
+ service: "Trying service command...",
6175
+ snap: "Trying snap..."
6176
+ };
6177
+ this.onProgress(`__progress__:${pct}:${msgs[strategy] || "Trying another approach..."}`);
6178
+ this.startDockerDaemon(strategy);
6179
+ } else {
6180
+ const msgs = [
6181
+ "Starting Docker...",
6182
+ "Waiting for Docker engine...",
6183
+ "Docker is loading...",
6184
+ "Almost there...",
6185
+ "Still starting up...",
6186
+ "First launch takes a bit longer...",
6187
+ "Hang tight..."
6188
+ ];
6189
+ const msgIdx = Math.floor(elapsed / 1e4) % msgs.length;
6190
+ this.onProgress(`__progress__:${pct}:${msgs[msgIdx]}`);
5992
6191
  }
5993
6192
  await new Promise((r) => setTimeout(r, 3e3));
5994
6193
  }
5995
6194
  throw new Error(
5996
- "Docker daemon did not start in time. Open Docker Desktop manually, wait for it to finish loading, then run this again."
6195
+ "DOCKER_MANUAL_START"
5997
6196
  );
5998
6197
  }
5999
6198
  /**
@@ -6010,14 +6209,17 @@ var DependencyInstaller = class {
6010
6209
  this.startDockerDaemon();
6011
6210
  await this.waitForDocker();
6012
6211
  }
6013
- this.onProgress("Starting Stalwart mail server...");
6014
- execFileSync2("docker", ["compose", "-f", composePath, "up", "-d"], {
6015
- timeout: 12e4,
6016
- stdio: ["ignore", "pipe", "pipe"]
6017
- });
6212
+ this.onProgress("__progress__:10:Pulling mail server image...");
6213
+ const composeResult = await runWithRollingOutput("docker", ["compose", "-f", composePath, "up", "-d"], { timeout: 12e4 });
6214
+ if (composeResult.exitCode !== 0) {
6215
+ throw new Error("Failed to start mail server container. Check Docker is running.");
6216
+ }
6217
+ this.onProgress("__progress__:60:Waiting for mail server to start...");
6018
6218
  const maxWait = 3e4;
6019
6219
  const start = Date.now();
6020
6220
  while (Date.now() - start < maxWait) {
6221
+ const pct = 60 + Math.round((Date.now() - start) / maxWait * 35);
6222
+ this.onProgress(`__progress__:${Math.min(95, pct)}:Starting mail server...`);
6021
6223
  try {
6022
6224
  const output = execFileSync2(
6023
6225
  "docker",
@@ -6025,6 +6227,7 @@ var DependencyInstaller = class {
6025
6227
  { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
6026
6228
  ).toString().trim();
6027
6229
  if (output.toLowerCase().includes("up")) {
6230
+ this.onProgress("__progress__:100:Mail server ready!");
6028
6231
  return;
6029
6232
  }
6030
6233
  } catch {
@@ -6098,6 +6301,440 @@ var DependencyInstaller = class {
6098
6301
  }
6099
6302
  };
6100
6303
 
6304
+ // src/setup/service.ts
6305
+ import { execFileSync as execFileSync3, execSync as execSync2 } from "child_process";
6306
+ import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync3, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
6307
+ import { join as join7 } from "path";
6308
+ import { homedir as homedir6, platform as platform3 } from "os";
6309
+ var PLIST_LABEL = "com.agenticmail.server";
6310
+ var SYSTEMD_UNIT = "agenticmail.service";
6311
+ var ServiceManager = class {
6312
+ os = platform3();
6313
+ /**
6314
+ * Get the path to the service file.
6315
+ */
6316
+ getServicePath() {
6317
+ if (this.os === "darwin") {
6318
+ return join7(homedir6(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
6319
+ } else {
6320
+ return join7(homedir6(), ".config", "systemd", "user", SYSTEMD_UNIT);
6321
+ }
6322
+ }
6323
+ /**
6324
+ * Find the Node.js binary path.
6325
+ */
6326
+ getNodePath() {
6327
+ try {
6328
+ return execFileSync3("which", ["node"], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6329
+ } catch {
6330
+ return process.execPath;
6331
+ }
6332
+ }
6333
+ /**
6334
+ * Find the API server entry point.
6335
+ * Searches common locations where agenticmail is installed.
6336
+ */
6337
+ getApiEntryPath() {
6338
+ const searchDirs = [
6339
+ // Global npm install
6340
+ join7(homedir6(), "node_modules", "agenticmail"),
6341
+ // npx cache / global prefix
6342
+ ...(() => {
6343
+ try {
6344
+ const prefix = execSync2("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6345
+ return [
6346
+ join7(prefix, "lib", "node_modules", "agenticmail"),
6347
+ join7(prefix, "node_modules", "agenticmail")
6348
+ ];
6349
+ } catch {
6350
+ return [];
6351
+ }
6352
+ })(),
6353
+ // Homebrew on macOS
6354
+ "/opt/homebrew/lib/node_modules/agenticmail",
6355
+ "/usr/local/lib/node_modules/agenticmail"
6356
+ ];
6357
+ for (const base of searchDirs) {
6358
+ const apiPaths = [
6359
+ join7(base, "node_modules", "@agenticmail", "api", "dist", "index.js"),
6360
+ join7(base, "..", "@agenticmail", "api", "dist", "index.js")
6361
+ ];
6362
+ for (const p of apiPaths) {
6363
+ if (existsSync5(p)) return p;
6364
+ }
6365
+ }
6366
+ const dataDir = join7(homedir6(), ".agenticmail");
6367
+ const entryCache = join7(dataDir, "api-entry.path");
6368
+ if (existsSync5(entryCache)) {
6369
+ const cached = readFileSync2(entryCache, "utf-8").trim();
6370
+ if (existsSync5(cached)) return cached;
6371
+ }
6372
+ throw new Error("Could not find @agenticmail/api entry point. Run `agenticmail start` first to populate the cache.");
6373
+ }
6374
+ /**
6375
+ * Cache the API entry path so the service can find it later.
6376
+ */
6377
+ cacheApiEntryPath(entryPath) {
6378
+ const dataDir = join7(homedir6(), ".agenticmail");
6379
+ if (!existsSync5(dataDir)) mkdirSync3(dataDir, { recursive: true });
6380
+ writeFileSync3(join7(dataDir, "api-entry.path"), entryPath);
6381
+ }
6382
+ /**
6383
+ * Get the current package version.
6384
+ */
6385
+ getVersion() {
6386
+ try {
6387
+ const pkgPaths = [
6388
+ join7(homedir6(), "node_modules", "agenticmail", "package.json"),
6389
+ join7(homedir6(), ".agenticmail", "package-version.json")
6390
+ ];
6391
+ try {
6392
+ const prefix = execSync2("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6393
+ pkgPaths.push(join7(prefix, "lib", "node_modules", "agenticmail", "package.json"));
6394
+ } catch {
6395
+ }
6396
+ for (const p of pkgPaths) {
6397
+ if (existsSync5(p)) {
6398
+ const pkg = JSON.parse(readFileSync2(p, "utf-8"));
6399
+ if (pkg.version) return pkg.version;
6400
+ }
6401
+ }
6402
+ } catch {
6403
+ }
6404
+ return "unknown";
6405
+ }
6406
+ /**
6407
+ * Generate a wrapper script that waits for Docker before starting the API.
6408
+ * This ensures AgenticMail doesn't fail on boot when Docker is still loading.
6409
+ */
6410
+ generateStartScript(nodePath, apiEntry) {
6411
+ const scriptPath = join7(homedir6(), ".agenticmail", "bin", "start-server.sh");
6412
+ const scriptDir = join7(homedir6(), ".agenticmail", "bin");
6413
+ if (!existsSync5(scriptDir)) mkdirSync3(scriptDir, { recursive: true });
6414
+ const script = [
6415
+ "#!/bin/bash",
6416
+ "# AgenticMail auto-start script",
6417
+ "# Waits for Docker to be ready, then starts the API server.",
6418
+ "",
6419
+ 'LOG_DIR="$HOME/.agenticmail/logs"',
6420
+ 'mkdir -p "$LOG_DIR"',
6421
+ "",
6422
+ "log() {",
6423
+ ` echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_DIR/startup.log"`,
6424
+ "}",
6425
+ "",
6426
+ 'log "AgenticMail starting..."',
6427
+ "",
6428
+ "# Wait for Docker daemon (up to 10 minutes \u2014 Docker Desktop can be very slow on first boot)",
6429
+ "MAX_WAIT=600",
6430
+ "WAITED=0",
6431
+ "while ! docker info >/dev/null 2>&1; do",
6432
+ " if [ $WAITED -ge $MAX_WAIT ]; then",
6433
+ ' log "ERROR: Docker did not start after ${MAX_WAIT}s. Exiting."',
6434
+ " exit 1",
6435
+ " fi",
6436
+ " sleep 5",
6437
+ " WAITED=$((WAITED + 5))",
6438
+ ' log "Waiting for Docker... (${WAITED}s)"',
6439
+ "done",
6440
+ 'log "Docker is ready (waited ${WAITED}s)"',
6441
+ "",
6442
+ "# Wait for Stalwart container (up to 60s)",
6443
+ "MAX_STALWART=60",
6444
+ "WAITED=0",
6445
+ 'while ! docker ps --filter "name=agenticmail-stalwart" --format "{{.Status}}" 2>/dev/null | grep -qi "up"; do',
6446
+ " if [ $WAITED -ge $MAX_STALWART ]; then",
6447
+ ' log "WARNING: Stalwart not running. Attempting to start..."',
6448
+ ' COMPOSE="$HOME/.agenticmail/docker-compose.yml"',
6449
+ ' if [ -f "$COMPOSE" ]; then',
6450
+ ' docker compose -f "$COMPOSE" up -d 2>>"$LOG_DIR/startup.log"',
6451
+ " sleep 5",
6452
+ " fi",
6453
+ " break",
6454
+ " fi",
6455
+ " sleep 3",
6456
+ " WAITED=$((WAITED + 3))",
6457
+ "done",
6458
+ 'log "Stalwart check complete"',
6459
+ "",
6460
+ "# Start the API server",
6461
+ `log "Starting API server: ${nodePath} ${apiEntry}"`,
6462
+ `exec "${nodePath}" "${apiEntry}"`
6463
+ ].join("\n") + "\n";
6464
+ writeFileSync3(scriptPath, script, { mode: 493 });
6465
+ return scriptPath;
6466
+ }
6467
+ /**
6468
+ * Generate the launchd plist content for macOS.
6469
+ * More robust than OpenClaw's plist:
6470
+ * - Wrapper script waits for Docker + Stalwart before starting
6471
+ * - KeepAlive: true (unconditional — always restart, not just on crash)
6472
+ * - SoftResourceLimits for file descriptors (email servers need many)
6473
+ * - StartInterval as backup heartbeat (checks every 5 min)
6474
+ * - Service version tracking in env vars
6475
+ */
6476
+ generatePlist(nodePath, apiEntry, configPath) {
6477
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
6478
+ const logDir = join7(homedir6(), ".agenticmail", "logs");
6479
+ if (!existsSync5(logDir)) mkdirSync3(logDir, { recursive: true });
6480
+ const version = this.getVersion();
6481
+ const startScript = this.generateStartScript(nodePath, apiEntry);
6482
+ return `<?xml version="1.0" encoding="UTF-8"?>
6483
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
6484
+ <plist version="1.0">
6485
+ <dict>
6486
+ <key>Label</key>
6487
+ <string>${PLIST_LABEL}</string>
6488
+
6489
+ <key>Comment</key>
6490
+ <string>AgenticMail API Server (v${version})</string>
6491
+
6492
+ <key>ProgramArguments</key>
6493
+ <array>
6494
+ <string>${startScript}</string>
6495
+ </array>
6496
+
6497
+ <key>EnvironmentVariables</key>
6498
+ <dict>
6499
+ <key>HOME</key>
6500
+ <string>${homedir6()}</string>
6501
+ <key>AGENTICMAIL_DATA_DIR</key>
6502
+ <string>${config.dataDir || join7(homedir6(), ".agenticmail")}</string>
6503
+ <key>AGENTICMAIL_MASTER_KEY</key>
6504
+ <string>${config.masterKey}</string>
6505
+ <key>STALWART_ADMIN_USER</key>
6506
+ <string>${config.stalwart.adminUser}</string>
6507
+ <key>STALWART_ADMIN_PASSWORD</key>
6508
+ <string>${config.stalwart.adminPassword}</string>
6509
+ <key>STALWART_URL</key>
6510
+ <string>${config.stalwart.url}</string>
6511
+ <key>AGENTICMAIL_API_PORT</key>
6512
+ <string>${String(config.api.port)}</string>
6513
+ <key>AGENTICMAIL_API_HOST</key>
6514
+ <string>${config.api.host}</string>
6515
+ <key>SMTP_HOST</key>
6516
+ <string>${config.smtp.host}</string>
6517
+ <key>SMTP_PORT</key>
6518
+ <string>${String(config.smtp.port)}</string>
6519
+ <key>IMAP_HOST</key>
6520
+ <string>${config.imap.host}</string>
6521
+ <key>IMAP_PORT</key>
6522
+ <string>${String(config.imap.port)}</string>
6523
+ <key>PATH</key>
6524
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
6525
+ <key>AGENTICMAIL_SERVICE_VERSION</key>
6526
+ <string>${version}</string>
6527
+ <key>AGENTICMAIL_SERVICE_LABEL</key>
6528
+ <string>${PLIST_LABEL}</string>
6529
+ </dict>
6530
+
6531
+ <!-- Start when user logs in -->
6532
+ <key>RunAtLoad</key>
6533
+ <true/>
6534
+
6535
+ <!-- Always keep running \u2014 restart unconditionally if it ever stops -->
6536
+ <key>KeepAlive</key>
6537
+ <true/>
6538
+
6539
+ <!-- Minimum 15s between restarts to avoid rapid crash loops -->
6540
+ <key>ThrottleInterval</key>
6541
+ <integer>15</integer>
6542
+
6543
+ <!-- File descriptor limits \u2014 email servers need many open connections -->
6544
+ <key>SoftResourceLimits</key>
6545
+ <dict>
6546
+ <key>NumberOfFiles</key>
6547
+ <integer>8192</integer>
6548
+ </dict>
6549
+ <key>HardResourceLimits</key>
6550
+ <dict>
6551
+ <key>NumberOfFiles</key>
6552
+ <integer>16384</integer>
6553
+ </dict>
6554
+
6555
+ <key>StandardOutPath</key>
6556
+ <string>${logDir}/server.log</string>
6557
+ <key>StandardErrorPath</key>
6558
+ <string>${logDir}/server.err.log</string>
6559
+
6560
+ <key>ProcessType</key>
6561
+ <string>Background</string>
6562
+ </dict>
6563
+ </plist>`;
6564
+ }
6565
+ /**
6566
+ * Generate the systemd user service content for Linux.
6567
+ * More robust than basic services:
6568
+ * - Wrapper script waits for Docker + Stalwart
6569
+ * - Restart=always (unconditional)
6570
+ * - WatchdogSec for health monitoring
6571
+ * - File descriptor limits
6572
+ * - Proper dependency ordering
6573
+ */
6574
+ generateSystemdUnit(nodePath, apiEntry, configPath) {
6575
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
6576
+ const dataDir = config.dataDir || join7(homedir6(), ".agenticmail");
6577
+ const version = this.getVersion();
6578
+ const startScript = this.generateStartScript(nodePath, apiEntry);
6579
+ return `[Unit]
6580
+ Description=AgenticMail API Server (v${version})
6581
+ After=network-online.target docker.service
6582
+ Wants=network-online.target docker.service
6583
+ StartLimitIntervalSec=300
6584
+ StartLimitBurst=5
6585
+
6586
+ [Service]
6587
+ Type=simple
6588
+ ExecStart=${startScript}
6589
+ Restart=always
6590
+ RestartSec=15
6591
+ TimeoutStartSec=660
6592
+ LimitNOFILE=8192
6593
+ Environment=HOME=${homedir6()}
6594
+ Environment=AGENTICMAIL_DATA_DIR=${dataDir}
6595
+ Environment=AGENTICMAIL_MASTER_KEY=${config.masterKey}
6596
+ Environment=STALWART_ADMIN_USER=${config.stalwart.adminUser}
6597
+ Environment=STALWART_ADMIN_PASSWORD=${config.stalwart.adminPassword}
6598
+ Environment=STALWART_URL=${config.stalwart.url}
6599
+ Environment=AGENTICMAIL_API_PORT=${config.api.port}
6600
+ Environment=AGENTICMAIL_API_HOST=${config.api.host}
6601
+ Environment=SMTP_HOST=${config.smtp.host}
6602
+ Environment=SMTP_PORT=${config.smtp.port}
6603
+ Environment=IMAP_HOST=${config.imap.host}
6604
+ Environment=IMAP_PORT=${config.imap.port}
6605
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin
6606
+ Environment=AGENTICMAIL_SERVICE_VERSION=${version}
6607
+
6608
+ [Install]
6609
+ WantedBy=default.target
6610
+ `;
6611
+ }
6612
+ /**
6613
+ * Install the auto-start service.
6614
+ */
6615
+ install() {
6616
+ const configPath = join7(homedir6(), ".agenticmail", "config.json");
6617
+ if (!existsSync5(configPath)) {
6618
+ return { installed: false, message: "Config not found. Run agenticmail setup first." };
6619
+ }
6620
+ const nodePath = this.getNodePath();
6621
+ let apiEntry;
6622
+ try {
6623
+ apiEntry = this.getApiEntryPath();
6624
+ } catch (err) {
6625
+ return { installed: false, message: err.message };
6626
+ }
6627
+ const servicePath = this.getServicePath();
6628
+ if (this.os === "darwin") {
6629
+ const dir = join7(homedir6(), "Library", "LaunchAgents");
6630
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
6631
+ if (existsSync5(servicePath)) {
6632
+ try {
6633
+ execFileSync3("launchctl", ["unload", servicePath], { timeout: 1e4, stdio: "ignore" });
6634
+ } catch {
6635
+ }
6636
+ }
6637
+ const plist = this.generatePlist(nodePath, apiEntry, configPath);
6638
+ writeFileSync3(servicePath, plist);
6639
+ try {
6640
+ execFileSync3("launchctl", ["load", servicePath], { timeout: 1e4, stdio: "ignore" });
6641
+ } catch (err) {
6642
+ return { installed: false, message: `Failed to load service: ${err.message}` };
6643
+ }
6644
+ return { installed: true, message: `Service installed at ${servicePath}` };
6645
+ } else if (this.os === "linux") {
6646
+ const dir = join7(homedir6(), ".config", "systemd", "user");
6647
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
6648
+ const unit = this.generateSystemdUnit(nodePath, apiEntry, configPath);
6649
+ writeFileSync3(servicePath, unit);
6650
+ try {
6651
+ execFileSync3("systemctl", ["--user", "daemon-reload"], { timeout: 1e4, stdio: "ignore" });
6652
+ execFileSync3("systemctl", ["--user", "enable", SYSTEMD_UNIT], { timeout: 1e4, stdio: "ignore" });
6653
+ execFileSync3("systemctl", ["--user", "start", SYSTEMD_UNIT], { timeout: 1e4, stdio: "ignore" });
6654
+ try {
6655
+ execFileSync3("loginctl", ["enable-linger"], { timeout: 1e4, stdio: "ignore" });
6656
+ } catch {
6657
+ }
6658
+ } catch (err) {
6659
+ return { installed: false, message: `Failed to enable service: ${err.message}` };
6660
+ }
6661
+ return { installed: true, message: `Service installed at ${servicePath}` };
6662
+ } else {
6663
+ return { installed: false, message: `Auto-start not supported on ${this.os}` };
6664
+ }
6665
+ }
6666
+ /**
6667
+ * Uninstall the auto-start service.
6668
+ */
6669
+ uninstall() {
6670
+ const servicePath = this.getServicePath();
6671
+ if (!existsSync5(servicePath)) {
6672
+ return { removed: false, message: "Service is not installed." };
6673
+ }
6674
+ if (this.os === "darwin") {
6675
+ try {
6676
+ execFileSync3("launchctl", ["unload", servicePath], { timeout: 1e4, stdio: "ignore" });
6677
+ } catch {
6678
+ }
6679
+ try {
6680
+ unlinkSync(servicePath);
6681
+ } catch {
6682
+ }
6683
+ return { removed: true, message: "Service removed." };
6684
+ } else if (this.os === "linux") {
6685
+ try {
6686
+ execFileSync3("systemctl", ["--user", "stop", SYSTEMD_UNIT], { timeout: 1e4, stdio: "ignore" });
6687
+ execFileSync3("systemctl", ["--user", "disable", SYSTEMD_UNIT], { timeout: 1e4, stdio: "ignore" });
6688
+ } catch {
6689
+ }
6690
+ try {
6691
+ unlinkSync(servicePath);
6692
+ } catch {
6693
+ }
6694
+ try {
6695
+ execFileSync3("systemctl", ["--user", "daemon-reload"], { timeout: 1e4, stdio: "ignore" });
6696
+ } catch {
6697
+ }
6698
+ return { removed: true, message: "Service removed." };
6699
+ } else {
6700
+ return { removed: false, message: `Not supported on ${this.os}` };
6701
+ }
6702
+ }
6703
+ /**
6704
+ * Get the current service status.
6705
+ */
6706
+ status() {
6707
+ const servicePath = this.getServicePath();
6708
+ const plat = this.os === "darwin" ? "launchd" : this.os === "linux" ? "systemd" : "unsupported";
6709
+ const installed = existsSync5(servicePath);
6710
+ let running = false;
6711
+ if (installed) {
6712
+ if (this.os === "darwin") {
6713
+ try {
6714
+ const output = execSync2(`launchctl list | grep ${PLIST_LABEL}`, { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString();
6715
+ const pid = output.trim().split(" ")[0];
6716
+ running = pid !== "-" && pid !== "" && !isNaN(parseInt(pid));
6717
+ } catch {
6718
+ }
6719
+ } else if (this.os === "linux") {
6720
+ try {
6721
+ execFileSync3("systemctl", ["--user", "is-active", SYSTEMD_UNIT], { timeout: 5e3, stdio: "ignore" });
6722
+ running = true;
6723
+ } catch {
6724
+ }
6725
+ }
6726
+ }
6727
+ return { installed, running, platform: plat, servicePath: installed ? servicePath : null };
6728
+ }
6729
+ /**
6730
+ * Reinstall the service (useful after config changes or updates).
6731
+ */
6732
+ reinstall() {
6733
+ this.uninstall();
6734
+ return this.install();
6735
+ }
6736
+ };
6737
+
6101
6738
  // src/setup/index.ts
6102
6739
  var SetupManager = class {
6103
6740
  checker = new DependencyChecker();
@@ -6139,13 +6776,13 @@ var SetupManager = class {
6139
6776
  * falls back to monorepo location.
6140
6777
  */
6141
6778
  getComposePath() {
6142
- const standalonePath = join7(homedir6(), ".agenticmail", "docker-compose.yml");
6143
- if (existsSync5(standalonePath)) return standalonePath;
6779
+ const standalonePath = join8(homedir7(), ".agenticmail", "docker-compose.yml");
6780
+ if (existsSync6(standalonePath)) return standalonePath;
6144
6781
  const cwd = process.cwd();
6145
- const candidates = [cwd, join7(cwd, "..")];
6782
+ const candidates = [cwd, join8(cwd, "..")];
6146
6783
  for (const dir of candidates) {
6147
- const p = join7(dir, "docker-compose.yml");
6148
- if (existsSync5(p)) return p;
6784
+ const p = join8(dir, "docker-compose.yml");
6785
+ if (existsSync6(p)) return p;
6149
6786
  }
6150
6787
  return standalonePath;
6151
6788
  }
@@ -6155,19 +6792,19 @@ var SetupManager = class {
6155
6792
  * Always regenerates Docker files to keep passwords in sync.
6156
6793
  */
6157
6794
  initConfig() {
6158
- const dataDir = join7(homedir6(), ".agenticmail");
6159
- const configPath = join7(dataDir, "config.json");
6160
- const envPath = join7(dataDir, ".env");
6161
- if (existsSync5(configPath)) {
6795
+ const dataDir = join8(homedir7(), ".agenticmail");
6796
+ const configPath = join8(dataDir, "config.json");
6797
+ const envPath = join8(dataDir, ".env");
6798
+ if (existsSync6(configPath)) {
6162
6799
  try {
6163
- const existing = JSON.parse(readFileSync2(configPath, "utf-8"));
6800
+ const existing = JSON.parse(readFileSync3(configPath, "utf-8"));
6164
6801
  this.generateDockerFiles(existing);
6165
6802
  return { configPath, envPath, config: existing, isNew: false };
6166
6803
  } catch {
6167
6804
  }
6168
6805
  }
6169
- if (!existsSync5(dataDir)) {
6170
- mkdirSync3(dataDir, { recursive: true });
6806
+ if (!existsSync6(dataDir)) {
6807
+ mkdirSync4(dataDir, { recursive: true });
6171
6808
  }
6172
6809
  const masterKey = `mk_${randomBytes2(24).toString("hex")}`;
6173
6810
  const stalwartPassword = randomBytes2(16).toString("hex");
@@ -6183,7 +6820,7 @@ var SetupManager = class {
6183
6820
  api: { port: 3100, host: "127.0.0.1" },
6184
6821
  dataDir
6185
6822
  };
6186
- writeFileSync3(configPath, JSON.stringify(config, null, 2));
6823
+ writeFileSync4(configPath, JSON.stringify(config, null, 2));
6187
6824
  chmodSync(configPath, 384);
6188
6825
  const envContent = `# Auto-generated by agenticmail setup
6189
6826
  STALWART_ADMIN_USER=admin
@@ -6199,7 +6836,7 @@ SMTP_PORT=587
6199
6836
  IMAP_HOST=localhost
6200
6837
  IMAP_PORT=143
6201
6838
  `;
6202
- writeFileSync3(envPath, envContent);
6839
+ writeFileSync4(envPath, envContent);
6203
6840
  chmodSync(envPath, 384);
6204
6841
  this.generateDockerFiles(config);
6205
6842
  return { configPath, envPath, config, isNew: true };
@@ -6209,13 +6846,13 @@ IMAP_PORT=143
6209
6846
  * with the correct admin password from config.
6210
6847
  */
6211
6848
  generateDockerFiles(config) {
6212
- const dataDir = config.dataDir || join7(homedir6(), ".agenticmail");
6213
- if (!existsSync5(dataDir)) {
6214
- mkdirSync3(dataDir, { recursive: true });
6849
+ const dataDir = config.dataDir || join8(homedir7(), ".agenticmail");
6850
+ if (!existsSync6(dataDir)) {
6851
+ mkdirSync4(dataDir, { recursive: true });
6215
6852
  }
6216
6853
  const password = config.stalwart?.adminPassword || "changeme";
6217
- const composePath = join7(dataDir, "docker-compose.yml");
6218
- writeFileSync3(composePath, `services:
6854
+ const composePath = join8(dataDir, "docker-compose.yml");
6855
+ writeFileSync4(composePath, `services:
6219
6856
  stalwart:
6220
6857
  image: stalwartlabs/stalwart:latest
6221
6858
  container_name: agenticmail-stalwart
@@ -6237,8 +6874,8 @@ IMAP_PORT=143
6237
6874
  volumes:
6238
6875
  stalwart-data:
6239
6876
  `);
6240
- const tomlPath = join7(dataDir, "stalwart.toml");
6241
- writeFileSync3(tomlPath, `# Stalwart Mail Server \u2014 AgenticMail Configuration
6877
+ const tomlPath = join8(dataDir, "stalwart.toml");
6878
+ writeFileSync4(tomlPath, `# Stalwart Mail Server \u2014 AgenticMail Configuration
6242
6879
 
6243
6880
  [server]
6244
6881
  hostname = "localhost"
@@ -6293,8 +6930,8 @@ secret = "${password}"
6293
6930
  * Check if config has already been initialized.
6294
6931
  */
6295
6932
  isInitialized() {
6296
- const configPath = join7(homedir6(), ".agenticmail", "config.json");
6297
- return existsSync5(configPath);
6933
+ const configPath = join8(homedir7(), ".agenticmail", "config.json");
6934
+ return existsSync6(configPath);
6298
6935
  }
6299
6936
  };
6300
6937
  export {
@@ -6319,6 +6956,7 @@ export {
6319
6956
  RelayBridge,
6320
6957
  RelayGateway,
6321
6958
  SPAM_THRESHOLD,
6959
+ ServiceManager,
6322
6960
  SetupManager,
6323
6961
  SmsManager,
6324
6962
  SmsPoller,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Core SDK for AgenticMail — programmatic email for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",