@agenticmail/core 0.5.58 → 0.5.61

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.cjs CHANGED
@@ -1918,13 +1918,14 @@ var AccountManager = class {
1918
1918
  }
1919
1919
  const principalName = options.name.toLowerCase();
1920
1920
  const email = `${principalName}@${domain}`;
1921
+ const existingAgent = await this.getByName(options.name);
1922
+ if (existingAgent != null) {
1923
+ throw new Error(`Account already exists: ${options.name}`);
1924
+ }
1921
1925
  await this.stalwart.ensureDomain(domain);
1922
- const existsInSqlite = await this.getByName(options.name) != null;
1923
- if (!existsInSqlite) {
1924
- try {
1925
- await this.stalwart.deletePrincipal(principalName);
1926
- } catch {
1927
- }
1926
+ try {
1927
+ await this.stalwart.deletePrincipal(principalName);
1928
+ } catch {
1928
1929
  }
1929
1930
  await this.stalwart.createPrincipal({
1930
1931
  type: "individual",
@@ -3652,6 +3653,40 @@ var import_nodemailer2 = __toESM(require("nodemailer"), 1);
3652
3653
  var import_mail_composer2 = __toESM(require("nodemailer/lib/mail-composer/index.js"), 1);
3653
3654
  var import_imapflow3 = require("imapflow");
3654
3655
  var import_mailparser2 = require("mailparser");
3656
+ function formatPollError(err) {
3657
+ if (!err) return "unknown error (no error object)";
3658
+ if (typeof err !== "object") return String(err);
3659
+ const e = err;
3660
+ const parts = [];
3661
+ const head = (e.message ?? String(err)).toString().trim();
3662
+ if (head) parts.push(head);
3663
+ if (e.code && e.code !== head) parts.push(`code=${e.code}`);
3664
+ if (typeof e.errno === "number") parts.push(`errno=${e.errno}`);
3665
+ if (e.syscall) parts.push(`syscall=${e.syscall}`);
3666
+ if (e.hostname) parts.push(`host=${e.hostname}`);
3667
+ if (typeof e.port === "number") parts.push(`port=${e.port}`);
3668
+ if (e.responseText && e.responseText !== head) {
3669
+ parts.push(`response=${truncate(String(e.responseText), 240)}`);
3670
+ } else if (e.response && e.response !== head) {
3671
+ parts.push(`response=${truncate(String(e.response), 240)}`);
3672
+ }
3673
+ if (e.command) parts.push(`command=${e.command}`);
3674
+ if (typeof e.exitCode === "number") parts.push(`exit=${e.exitCode}`);
3675
+ else if (e.code && /^\d+$/.test(String(e.code))) parts.push(`exit=${e.code}`);
3676
+ if (e.signal) parts.push(`signal=${e.signal}`);
3677
+ const stderr = (e.stderr ?? "").toString().trim();
3678
+ if (stderr) parts.push(`stderr=${truncate(stderr, 240)}`);
3679
+ const stdout = (e.stdout ?? "").toString().trim();
3680
+ if (stdout && !stderr) parts.push(`stdout=${truncate(stdout, 240)}`);
3681
+ if (parts.length === 1 && /^command failed$/i.test(head)) {
3682
+ return `${head} (no further detail available \u2014 wrapping error did not carry stderr/code/response)`;
3683
+ }
3684
+ return parts.join(" | ");
3685
+ }
3686
+ function truncate(s, max) {
3687
+ if (s.length <= max) return s;
3688
+ return s.slice(0, max - 1).trimEnd() + "\u2026";
3689
+ }
3655
3690
  var RelayGateway = class {
3656
3691
  smtpTransport = null;
3657
3692
  pollTimer = null;
@@ -3826,7 +3861,7 @@ var RelayGateway = class {
3826
3861
  this.consecutiveFailures = 0;
3827
3862
  } catch (err) {
3828
3863
  this.consecutiveFailures++;
3829
- const msg = err instanceof Error ? err.message : String(err);
3864
+ const msg = formatPollError(err);
3830
3865
  console.error(`[RelayGateway] Poll failed (attempt ${this.consecutiveFailures}): ${msg}`);
3831
3866
  if (this.consecutiveFailures >= 5 && this.consecutiveFailures % 5 === 0) {
3832
3867
  console.error(`[RelayGateway] ${this.consecutiveFailures} consecutive failures \u2014 check IMAP credentials and connectivity (${this.config?.imapHost}:${this.config?.imapPort})`);
@@ -6084,20 +6119,23 @@ var GatewayManager = class {
6084
6119
  }
6085
6120
  /**
6086
6121
  * Resume gateway from saved config (e.g., after server restart).
6122
+ *
6123
+ * Issue #31 — On a Docker container restart the API can come up
6124
+ * before Stalwart / Gmail IMAP / DNS is reachable, so the very first
6125
+ * setup() can fail with a transient network error. Previously that
6126
+ * single failure was logged and never retried, leaving polling
6127
+ * permanently dead until someone noticed and manually revived the
6128
+ * relay. We now schedule background retries with exponential backoff
6129
+ * (5s, 10s, 20s, 40s, 60s cap, indefinite) so the relay
6130
+ * self-recovers as soon as the dependency is reachable again.
6087
6131
  */
6088
6132
  async resume() {
6089
6133
  if (this.config.mode === "relay" && this.config.relay) {
6090
6134
  try {
6091
- await this.relay.setup(this.config.relay);
6092
- const savedUid = this.loadLastSeenUid();
6093
- if (savedUid > 0) {
6094
- this.relay.setLastSeenUid(savedUid);
6095
- console.log(`[GatewayManager] Restored lastSeenUid=${savedUid} from database`);
6096
- }
6097
- this.relay.onUidAdvance = (uid) => this.saveLastSeenUid(uid);
6098
- await this.relay.startPolling();
6135
+ await this._resumeRelayOnce();
6099
6136
  } catch (err) {
6100
- console.error("[GatewayManager] Failed to resume relay:", err);
6137
+ console.error("[GatewayManager] Initial relay resume failed; scheduling retries:", formatPollError(err));
6138
+ this._scheduleRelayResumeRetry();
6101
6139
  }
6102
6140
  }
6103
6141
  if (this.smsManager && this.accountManager) {
@@ -6124,6 +6162,42 @@ var GatewayManager = class {
6124
6162
  }
6125
6163
  }
6126
6164
  }
6165
+ // ─── Issue #31 helpers — resume retry with backoff ───
6166
+ _resumeRetryTimer = null;
6167
+ _resumeRetryAttempt = 0;
6168
+ async _resumeRelayOnce() {
6169
+ if (!this.config.relay) throw new Error("No relay config to resume");
6170
+ await this.relay.setup(this.config.relay);
6171
+ const savedUid = this.loadLastSeenUid();
6172
+ if (savedUid > 0) {
6173
+ this.relay.setLastSeenUid(savedUid);
6174
+ console.log(`[GatewayManager] Restored lastSeenUid=${savedUid} from database`);
6175
+ }
6176
+ this.relay.onUidAdvance = (uid) => this.saveLastSeenUid(uid);
6177
+ await this.relay.startPolling();
6178
+ if (this._resumeRetryAttempt > 0) {
6179
+ console.log(`[GatewayManager] Relay polling resumed after ${this._resumeRetryAttempt} retry attempt${this._resumeRetryAttempt !== 1 ? "s" : ""}`);
6180
+ }
6181
+ this._resumeRetryAttempt = 0;
6182
+ }
6183
+ _scheduleRelayResumeRetry() {
6184
+ if (this._resumeRetryTimer) return;
6185
+ this._resumeRetryAttempt++;
6186
+ const base = Math.min(5e3 * Math.pow(2, this._resumeRetryAttempt - 1), 6e4);
6187
+ const jitter = base * (0.8 + Math.random() * 0.4);
6188
+ const delay = Math.round(jitter);
6189
+ console.log(`[GatewayManager] Will retry relay resume in ${(delay / 1e3).toFixed(1)}s (attempt ${this._resumeRetryAttempt + 1})`);
6190
+ this._resumeRetryTimer = setTimeout(async () => {
6191
+ this._resumeRetryTimer = null;
6192
+ if (this.config.mode !== "relay" || !this.config.relay) return;
6193
+ try {
6194
+ await this._resumeRelayOnce();
6195
+ } catch (err) {
6196
+ console.error(`[GatewayManager] Relay resume retry ${this._resumeRetryAttempt} failed:`, formatPollError(err));
6197
+ this._scheduleRelayResumeRetry();
6198
+ }
6199
+ }, delay);
6200
+ }
6127
6201
  // --- Persistence ---
6128
6202
  loadConfig() {
6129
6203
  const row = this.db.prepare("SELECT * FROM gateway_config WHERE id = ?").get("default");
@@ -7076,6 +7150,8 @@ var import_node_child_process4 = require("child_process");
7076
7150
  var import_node_fs6 = require("fs");
7077
7151
  var import_node_path7 = require("path");
7078
7152
  var import_node_os6 = require("os");
7153
+ var import_node_module = require("module");
7154
+ var import_meta = {};
7079
7155
  var PLIST_LABEL = "com.agenticmail.server";
7080
7156
  var SYSTEMD_UNIT = "agenticmail.service";
7081
7157
  var ServiceManager = class {
@@ -7102,42 +7178,59 @@ var ServiceManager = class {
7102
7178
  }
7103
7179
  /**
7104
7180
  * Find the API server entry point.
7105
- * Searches common locations where agenticmail is installed.
7181
+ *
7182
+ * Issue #26 — Robust path resolution.
7183
+ *
7184
+ * The original implementation hard-coded `node_modules/agenticmail` (the
7185
+ * old unscoped package name). After the rename to `@agenticmail/cli`, that
7186
+ * directory no longer exists, so the resolver fell back to the stale
7187
+ * `~/.agenticmail/api-entry.path` cache and the launchd plist kept pointing
7188
+ * at a deleted path — causing the boot crash loop reported in #26.
7189
+ *
7190
+ * We now prefer `require.resolve('@agenticmail/api')` so the resolution
7191
+ * follows the actual installed location regardless of npm prefix, the
7192
+ * scoped vs unscoped package name, or the package manager (npm global,
7193
+ * pnpm, yarn global, local node_modules). Cached paths are always
7194
+ * validated against the filesystem before being returned.
7106
7195
  */
7107
7196
  getApiEntryPath() {
7108
- const searchDirs = [
7109
- // Global npm install
7110
- (0, import_node_path7.join)((0, import_node_os6.homedir)(), "node_modules", "agenticmail"),
7111
- // npx cache / global prefix
7112
- ...(() => {
7113
- try {
7114
- const prefix = (0, import_node_child_process4.execSync)("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
7115
- return [
7116
- (0, import_node_path7.join)(prefix, "lib", "node_modules", "agenticmail"),
7117
- (0, import_node_path7.join)(prefix, "node_modules", "agenticmail")
7118
- ];
7119
- } catch {
7120
- return [];
7121
- }
7122
- })(),
7123
- // Homebrew on macOS
7124
- "/opt/homebrew/lib/node_modules/agenticmail",
7125
- "/usr/local/lib/node_modules/agenticmail"
7197
+ try {
7198
+ const req = (0, import_node_module.createRequire)(import_meta.url);
7199
+ const resolved = req.resolve("@agenticmail/api");
7200
+ if ((0, import_node_fs6.existsSync)(resolved)) return resolved;
7201
+ } catch {
7202
+ }
7203
+ const parentPackages = [
7204
+ (0, import_node_path7.join)("@agenticmail", "cli"),
7205
+ // current scoped package
7206
+ "agenticmail"
7207
+ // legacy unscoped package
7126
7208
  ];
7127
- for (const base of searchDirs) {
7128
- const apiPaths = [
7129
- (0, import_node_path7.join)(base, "node_modules", "@agenticmail", "api", "dist", "index.js"),
7130
- (0, import_node_path7.join)(base, "..", "@agenticmail", "api", "dist", "index.js")
7131
- ];
7132
- for (const p of apiPaths) {
7133
- if ((0, import_node_fs6.existsSync)(p)) return p;
7209
+ const baseDirs = [
7210
+ // user-local install
7211
+ (0, import_node_path7.join)((0, import_node_os6.homedir)(), "node_modules")
7212
+ ];
7213
+ try {
7214
+ const prefix = (0, import_node_child_process4.execSync)("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
7215
+ baseDirs.push((0, import_node_path7.join)(prefix, "lib", "node_modules"));
7216
+ baseDirs.push((0, import_node_path7.join)(prefix, "node_modules"));
7217
+ } catch {
7218
+ }
7219
+ baseDirs.push("/opt/homebrew/lib/node_modules");
7220
+ baseDirs.push("/usr/local/lib/node_modules");
7221
+ for (const base of baseDirs) {
7222
+ const sibling = (0, import_node_path7.join)(base, "@agenticmail", "api", "dist", "index.js");
7223
+ if ((0, import_node_fs6.existsSync)(sibling)) return sibling;
7224
+ for (const parent of parentPackages) {
7225
+ const nested = (0, import_node_path7.join)(base, parent, "node_modules", "@agenticmail", "api", "dist", "index.js");
7226
+ if ((0, import_node_fs6.existsSync)(nested)) return nested;
7134
7227
  }
7135
7228
  }
7136
7229
  const dataDir = (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".agenticmail");
7137
7230
  const entryCache = (0, import_node_path7.join)(dataDir, "api-entry.path");
7138
7231
  if ((0, import_node_fs6.existsSync)(entryCache)) {
7139
7232
  const cached = (0, import_node_fs6.readFileSync)(entryCache, "utf-8").trim();
7140
- if ((0, import_node_fs6.existsSync)(cached)) return cached;
7233
+ if (cached && (0, import_node_fs6.existsSync)(cached)) return cached;
7141
7234
  }
7142
7235
  throw new Error("Could not find @agenticmail/api entry point. Run `agenticmail start` first to populate the cache.");
7143
7236
  }
@@ -7151,25 +7244,49 @@ var ServiceManager = class {
7151
7244
  }
7152
7245
  /**
7153
7246
  * Get the current package version.
7247
+ *
7248
+ * Issue #26 — resolve the CLI package.json via require.resolve so the
7249
+ * version reflects the *currently installed* @agenticmail/cli, not a
7250
+ * leftover unscoped `agenticmail` package directory.
7154
7251
  */
7155
7252
  getVersion() {
7156
7253
  try {
7157
- const pkgPaths = [
7158
- (0, import_node_path7.join)((0, import_node_os6.homedir)(), "node_modules", "agenticmail", "package.json"),
7159
- (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".agenticmail", "package-version.json")
7160
- ];
7161
- try {
7162
- const prefix = (0, import_node_child_process4.execSync)("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
7163
- pkgPaths.push((0, import_node_path7.join)(prefix, "lib", "node_modules", "agenticmail", "package.json"));
7164
- } catch {
7254
+ const req = (0, import_node_module.createRequire)(import_meta.url);
7255
+ const pkgJson = req.resolve("@agenticmail/cli/package.json");
7256
+ if ((0, import_node_fs6.existsSync)(pkgJson)) {
7257
+ const pkg = JSON.parse((0, import_node_fs6.readFileSync)(pkgJson, "utf-8"));
7258
+ if (pkg.version) return pkg.version;
7259
+ }
7260
+ } catch {
7261
+ }
7262
+ try {
7263
+ const apiEntry = this.getApiEntryPath();
7264
+ const apiPkg = (0, import_node_path7.join)(apiEntry, "..", "..", "package.json");
7265
+ if ((0, import_node_fs6.existsSync)(apiPkg)) {
7266
+ const pkg = JSON.parse((0, import_node_fs6.readFileSync)(apiPkg, "utf-8"));
7267
+ if (pkg.version) return pkg.version;
7165
7268
  }
7166
- for (const p of pkgPaths) {
7269
+ } catch {
7270
+ }
7271
+ const candidates = [
7272
+ (0, import_node_path7.join)((0, import_node_os6.homedir)(), "node_modules", "@agenticmail", "cli", "package.json"),
7273
+ (0, import_node_path7.join)((0, import_node_os6.homedir)(), "node_modules", "agenticmail", "package.json"),
7274
+ (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".agenticmail", "package-version.json")
7275
+ ];
7276
+ try {
7277
+ const prefix = (0, import_node_child_process4.execSync)("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
7278
+ candidates.push((0, import_node_path7.join)(prefix, "lib", "node_modules", "@agenticmail", "cli", "package.json"));
7279
+ candidates.push((0, import_node_path7.join)(prefix, "lib", "node_modules", "agenticmail", "package.json"));
7280
+ } catch {
7281
+ }
7282
+ for (const p of candidates) {
7283
+ try {
7167
7284
  if ((0, import_node_fs6.existsSync)(p)) {
7168
7285
  const pkg = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
7169
7286
  if (pkg.version) return pkg.version;
7170
7287
  }
7288
+ } catch {
7171
7289
  }
7172
- } catch {
7173
7290
  }
7174
7291
  return "unknown";
7175
7292
  }
@@ -7475,6 +7592,86 @@ WantedBy=default.target
7475
7592
  this.uninstall();
7476
7593
  return this.install();
7477
7594
  }
7595
+ /**
7596
+ * Issue #26 — Detect a stale service installation.
7597
+ *
7598
+ * Background: when a user upgrades from the old unscoped `agenticmail`
7599
+ * package to the new `@agenticmail/cli` scoped package, the old
7600
+ * ~/Library/LaunchAgents/com.agenticmail.server.plist and
7601
+ * ~/.agenticmail/bin/start-server.sh files keep pointing at
7602
+ * /opt/homebrew/lib/node_modules/agenticmail/... — a path that no longer
7603
+ * exists post-rename. The result is a launchd crash loop.
7604
+ *
7605
+ * `needsRepair()` returns a non-null reason whenever:
7606
+ * - the service file exists but the start-server.sh it launches is
7607
+ * missing or references a node_modules path that no longer resolves;
7608
+ * - the embedded service version drifts from the running CLI version
7609
+ * (so service files get refreshed on every upgrade — including
7610
+ * in-place version bumps that don't change the install path);
7611
+ * - the cached API entry path no longer exists on disk.
7612
+ *
7613
+ * Returns null when everything checks out — callers should treat that as
7614
+ * "no action needed".
7615
+ *
7616
+ * Platform-aware: only inspects launchd artefacts on darwin and systemd
7617
+ * artefacts on linux. Returns null on unsupported platforms so this can
7618
+ * be called unconditionally from the CLI's start path.
7619
+ */
7620
+ needsRepair() {
7621
+ if (this.os !== "darwin" && this.os !== "linux") return null;
7622
+ const servicePath = this.getServicePath();
7623
+ if (!(0, import_node_fs6.existsSync)(servicePath)) return null;
7624
+ let serviceContent = "";
7625
+ try {
7626
+ serviceContent = (0, import_node_fs6.readFileSync)(servicePath, "utf-8");
7627
+ } catch {
7628
+ return { reason: "Service file unreadable" };
7629
+ }
7630
+ const startScript = (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".agenticmail", "bin", "start-server.sh");
7631
+ if (serviceContent.includes(startScript)) {
7632
+ if (!(0, import_node_fs6.existsSync)(startScript)) {
7633
+ return { reason: "start-server.sh is missing" };
7634
+ }
7635
+ let scriptContent = "";
7636
+ try {
7637
+ scriptContent = (0, import_node_fs6.readFileSync)(startScript, "utf-8");
7638
+ } catch {
7639
+ return { reason: "start-server.sh unreadable" };
7640
+ }
7641
+ const apiPathMatch = scriptContent.match(/(\/[^"\s]+@agenticmail\/api\/dist\/index\.js)/);
7642
+ if (apiPathMatch) {
7643
+ const referenced = apiPathMatch[1];
7644
+ if (!(0, import_node_fs6.existsSync)(referenced)) {
7645
+ return { reason: `start-server.sh references missing path: ${referenced}` };
7646
+ }
7647
+ }
7648
+ if (/node_modules\/agenticmail\/(?!.*@agenticmail\/cli)/.test(scriptContent)) {
7649
+ const stale = /(\S*node_modules\/agenticmail\/\S*)/.exec(scriptContent)?.[1];
7650
+ if (stale && !(0, import_node_fs6.existsSync)(stale)) {
7651
+ return { reason: `start-server.sh references legacy unscoped path: ${stale}` };
7652
+ }
7653
+ }
7654
+ } else {
7655
+ return { reason: "Service file does not reference the wrapper script" };
7656
+ }
7657
+ const currentVersion = this.getVersion();
7658
+ if (currentVersion !== "unknown") {
7659
+ if (!serviceContent.includes(`v${currentVersion}`) || !serviceContent.includes(`AGENTICMAIL_SERVICE_VERSION`) || !serviceContent.includes(currentVersion)) {
7660
+ return { reason: `Service version drift (current CLI is v${currentVersion})` };
7661
+ }
7662
+ }
7663
+ const entryCache = (0, import_node_path7.join)((0, import_node_os6.homedir)(), ".agenticmail", "api-entry.path");
7664
+ if ((0, import_node_fs6.existsSync)(entryCache)) {
7665
+ try {
7666
+ const cached = (0, import_node_fs6.readFileSync)(entryCache, "utf-8").trim();
7667
+ if (cached && !(0, import_node_fs6.existsSync)(cached)) {
7668
+ return { reason: `Cached API entry path no longer exists: ${cached}` };
7669
+ }
7670
+ } catch {
7671
+ }
7672
+ }
7673
+ return null;
7674
+ }
7478
7675
  };
7479
7676
 
7480
7677
  // src/setup/index.ts
package/dist/index.d.cts CHANGED
@@ -1244,8 +1244,21 @@ declare class GatewayManager {
1244
1244
  shutdown(): Promise<void>;
1245
1245
  /**
1246
1246
  * Resume gateway from saved config (e.g., after server restart).
1247
+ *
1248
+ * Issue #31 — On a Docker container restart the API can come up
1249
+ * before Stalwart / Gmail IMAP / DNS is reachable, so the very first
1250
+ * setup() can fail with a transient network error. Previously that
1251
+ * single failure was logged and never retried, leaving polling
1252
+ * permanently dead until someone noticed and manually revived the
1253
+ * relay. We now schedule background retries with exponential backoff
1254
+ * (5s, 10s, 20s, 40s, 60s cap, indefinite) so the relay
1255
+ * self-recovers as soon as the dependency is reachable again.
1247
1256
  */
1248
1257
  resume(): Promise<void>;
1258
+ private _resumeRetryTimer;
1259
+ private _resumeRetryAttempt;
1260
+ private _resumeRelayOnce;
1261
+ private _scheduleRelayResumeRetry;
1249
1262
  private loadConfig;
1250
1263
  private saveConfig;
1251
1264
  private saveLastSeenUid;
@@ -1558,7 +1571,20 @@ declare class ServiceManager {
1558
1571
  private getNodePath;
1559
1572
  /**
1560
1573
  * Find the API server entry point.
1561
- * Searches common locations where agenticmail is installed.
1574
+ *
1575
+ * Issue #26 — Robust path resolution.
1576
+ *
1577
+ * The original implementation hard-coded `node_modules/agenticmail` (the
1578
+ * old unscoped package name). After the rename to `@agenticmail/cli`, that
1579
+ * directory no longer exists, so the resolver fell back to the stale
1580
+ * `~/.agenticmail/api-entry.path` cache and the launchd plist kept pointing
1581
+ * at a deleted path — causing the boot crash loop reported in #26.
1582
+ *
1583
+ * We now prefer `require.resolve('@agenticmail/api')` so the resolution
1584
+ * follows the actual installed location regardless of npm prefix, the
1585
+ * scoped vs unscoped package name, or the package manager (npm global,
1586
+ * pnpm, yarn global, local node_modules). Cached paths are always
1587
+ * validated against the filesystem before being returned.
1562
1588
  */
1563
1589
  private getApiEntryPath;
1564
1590
  /**
@@ -1567,6 +1593,10 @@ declare class ServiceManager {
1567
1593
  cacheApiEntryPath(entryPath: string): void;
1568
1594
  /**
1569
1595
  * Get the current package version.
1596
+ *
1597
+ * Issue #26 — resolve the CLI package.json via require.resolve so the
1598
+ * version reflects the *currently installed* @agenticmail/cli, not a
1599
+ * leftover unscoped `agenticmail` package directory.
1570
1600
  */
1571
1601
  private getVersion;
1572
1602
  /**
@@ -1619,6 +1649,34 @@ declare class ServiceManager {
1619
1649
  installed: boolean;
1620
1650
  message: string;
1621
1651
  };
1652
+ /**
1653
+ * Issue #26 — Detect a stale service installation.
1654
+ *
1655
+ * Background: when a user upgrades from the old unscoped `agenticmail`
1656
+ * package to the new `@agenticmail/cli` scoped package, the old
1657
+ * ~/Library/LaunchAgents/com.agenticmail.server.plist and
1658
+ * ~/.agenticmail/bin/start-server.sh files keep pointing at
1659
+ * /opt/homebrew/lib/node_modules/agenticmail/... — a path that no longer
1660
+ * exists post-rename. The result is a launchd crash loop.
1661
+ *
1662
+ * `needsRepair()` returns a non-null reason whenever:
1663
+ * - the service file exists but the start-server.sh it launches is
1664
+ * missing or references a node_modules path that no longer resolves;
1665
+ * - the embedded service version drifts from the running CLI version
1666
+ * (so service files get refreshed on every upgrade — including
1667
+ * in-place version bumps that don't change the install path);
1668
+ * - the cached API entry path no longer exists on disk.
1669
+ *
1670
+ * Returns null when everything checks out — callers should treat that as
1671
+ * "no action needed".
1672
+ *
1673
+ * Platform-aware: only inspects launchd artefacts on darwin and systemd
1674
+ * artefacts on linux. Returns null on unsupported platforms so this can
1675
+ * be called unconditionally from the CLI's start path.
1676
+ */
1677
+ needsRepair(): {
1678
+ reason: string;
1679
+ } | null;
1622
1680
  }
1623
1681
 
1624
1682
  interface SetupConfig {
package/dist/index.d.ts CHANGED
@@ -1244,8 +1244,21 @@ declare class GatewayManager {
1244
1244
  shutdown(): Promise<void>;
1245
1245
  /**
1246
1246
  * Resume gateway from saved config (e.g., after server restart).
1247
+ *
1248
+ * Issue #31 — On a Docker container restart the API can come up
1249
+ * before Stalwart / Gmail IMAP / DNS is reachable, so the very first
1250
+ * setup() can fail with a transient network error. Previously that
1251
+ * single failure was logged and never retried, leaving polling
1252
+ * permanently dead until someone noticed and manually revived the
1253
+ * relay. We now schedule background retries with exponential backoff
1254
+ * (5s, 10s, 20s, 40s, 60s cap, indefinite) so the relay
1255
+ * self-recovers as soon as the dependency is reachable again.
1247
1256
  */
1248
1257
  resume(): Promise<void>;
1258
+ private _resumeRetryTimer;
1259
+ private _resumeRetryAttempt;
1260
+ private _resumeRelayOnce;
1261
+ private _scheduleRelayResumeRetry;
1249
1262
  private loadConfig;
1250
1263
  private saveConfig;
1251
1264
  private saveLastSeenUid;
@@ -1558,7 +1571,20 @@ declare class ServiceManager {
1558
1571
  private getNodePath;
1559
1572
  /**
1560
1573
  * Find the API server entry point.
1561
- * Searches common locations where agenticmail is installed.
1574
+ *
1575
+ * Issue #26 — Robust path resolution.
1576
+ *
1577
+ * The original implementation hard-coded `node_modules/agenticmail` (the
1578
+ * old unscoped package name). After the rename to `@agenticmail/cli`, that
1579
+ * directory no longer exists, so the resolver fell back to the stale
1580
+ * `~/.agenticmail/api-entry.path` cache and the launchd plist kept pointing
1581
+ * at a deleted path — causing the boot crash loop reported in #26.
1582
+ *
1583
+ * We now prefer `require.resolve('@agenticmail/api')` so the resolution
1584
+ * follows the actual installed location regardless of npm prefix, the
1585
+ * scoped vs unscoped package name, or the package manager (npm global,
1586
+ * pnpm, yarn global, local node_modules). Cached paths are always
1587
+ * validated against the filesystem before being returned.
1562
1588
  */
1563
1589
  private getApiEntryPath;
1564
1590
  /**
@@ -1567,6 +1593,10 @@ declare class ServiceManager {
1567
1593
  cacheApiEntryPath(entryPath: string): void;
1568
1594
  /**
1569
1595
  * Get the current package version.
1596
+ *
1597
+ * Issue #26 — resolve the CLI package.json via require.resolve so the
1598
+ * version reflects the *currently installed* @agenticmail/cli, not a
1599
+ * leftover unscoped `agenticmail` package directory.
1570
1600
  */
1571
1601
  private getVersion;
1572
1602
  /**
@@ -1619,6 +1649,34 @@ declare class ServiceManager {
1619
1649
  installed: boolean;
1620
1650
  message: string;
1621
1651
  };
1652
+ /**
1653
+ * Issue #26 — Detect a stale service installation.
1654
+ *
1655
+ * Background: when a user upgrades from the old unscoped `agenticmail`
1656
+ * package to the new `@agenticmail/cli` scoped package, the old
1657
+ * ~/Library/LaunchAgents/com.agenticmail.server.plist and
1658
+ * ~/.agenticmail/bin/start-server.sh files keep pointing at
1659
+ * /opt/homebrew/lib/node_modules/agenticmail/... — a path that no longer
1660
+ * exists post-rename. The result is a launchd crash loop.
1661
+ *
1662
+ * `needsRepair()` returns a non-null reason whenever:
1663
+ * - the service file exists but the start-server.sh it launches is
1664
+ * missing or references a node_modules path that no longer resolves;
1665
+ * - the embedded service version drifts from the running CLI version
1666
+ * (so service files get refreshed on every upgrade — including
1667
+ * in-place version bumps that don't change the install path);
1668
+ * - the cached API entry path no longer exists on disk.
1669
+ *
1670
+ * Returns null when everything checks out — callers should treat that as
1671
+ * "no action needed".
1672
+ *
1673
+ * Platform-aware: only inspects launchd artefacts on darwin and systemd
1674
+ * artefacts on linux. Returns null on unsupported platforms so this can
1675
+ * be called unconditionally from the CLI's start path.
1676
+ */
1677
+ needsRepair(): {
1678
+ reason: string;
1679
+ } | null;
1622
1680
  }
1623
1681
 
1624
1682
  interface SetupConfig {
package/dist/index.js CHANGED
@@ -1164,13 +1164,14 @@ var AccountManager = class {
1164
1164
  }
1165
1165
  const principalName = options.name.toLowerCase();
1166
1166
  const email = `${principalName}@${domain}`;
1167
+ const existingAgent = await this.getByName(options.name);
1168
+ if (existingAgent != null) {
1169
+ throw new Error(`Account already exists: ${options.name}`);
1170
+ }
1167
1171
  await this.stalwart.ensureDomain(domain);
1168
- const existsInSqlite = await this.getByName(options.name) != null;
1169
- if (!existsInSqlite) {
1170
- try {
1171
- await this.stalwart.deletePrincipal(principalName);
1172
- } catch {
1173
- }
1172
+ try {
1173
+ await this.stalwart.deletePrincipal(principalName);
1174
+ } catch {
1174
1175
  }
1175
1176
  await this.stalwart.createPrincipal({
1176
1177
  type: "individual",
@@ -2895,6 +2896,40 @@ import nodemailer2 from "nodemailer";
2895
2896
  import MailComposer2 from "nodemailer/lib/mail-composer/index.js";
2896
2897
  import { ImapFlow as ImapFlow3 } from "imapflow";
2897
2898
  import { simpleParser as simpleParser2 } from "mailparser";
2899
+ function formatPollError(err) {
2900
+ if (!err) return "unknown error (no error object)";
2901
+ if (typeof err !== "object") return String(err);
2902
+ const e = err;
2903
+ const parts = [];
2904
+ const head = (e.message ?? String(err)).toString().trim();
2905
+ if (head) parts.push(head);
2906
+ if (e.code && e.code !== head) parts.push(`code=${e.code}`);
2907
+ if (typeof e.errno === "number") parts.push(`errno=${e.errno}`);
2908
+ if (e.syscall) parts.push(`syscall=${e.syscall}`);
2909
+ if (e.hostname) parts.push(`host=${e.hostname}`);
2910
+ if (typeof e.port === "number") parts.push(`port=${e.port}`);
2911
+ if (e.responseText && e.responseText !== head) {
2912
+ parts.push(`response=${truncate(String(e.responseText), 240)}`);
2913
+ } else if (e.response && e.response !== head) {
2914
+ parts.push(`response=${truncate(String(e.response), 240)}`);
2915
+ }
2916
+ if (e.command) parts.push(`command=${e.command}`);
2917
+ if (typeof e.exitCode === "number") parts.push(`exit=${e.exitCode}`);
2918
+ else if (e.code && /^\d+$/.test(String(e.code))) parts.push(`exit=${e.code}`);
2919
+ if (e.signal) parts.push(`signal=${e.signal}`);
2920
+ const stderr = (e.stderr ?? "").toString().trim();
2921
+ if (stderr) parts.push(`stderr=${truncate(stderr, 240)}`);
2922
+ const stdout = (e.stdout ?? "").toString().trim();
2923
+ if (stdout && !stderr) parts.push(`stdout=${truncate(stdout, 240)}`);
2924
+ if (parts.length === 1 && /^command failed$/i.test(head)) {
2925
+ return `${head} (no further detail available \u2014 wrapping error did not carry stderr/code/response)`;
2926
+ }
2927
+ return parts.join(" | ");
2928
+ }
2929
+ function truncate(s, max) {
2930
+ if (s.length <= max) return s;
2931
+ return s.slice(0, max - 1).trimEnd() + "\u2026";
2932
+ }
2898
2933
  var RelayGateway = class {
2899
2934
  smtpTransport = null;
2900
2935
  pollTimer = null;
@@ -3069,7 +3104,7 @@ var RelayGateway = class {
3069
3104
  this.consecutiveFailures = 0;
3070
3105
  } catch (err) {
3071
3106
  this.consecutiveFailures++;
3072
- const msg = err instanceof Error ? err.message : String(err);
3107
+ const msg = formatPollError(err);
3073
3108
  console.error(`[RelayGateway] Poll failed (attempt ${this.consecutiveFailures}): ${msg}`);
3074
3109
  if (this.consecutiveFailures >= 5 && this.consecutiveFailures % 5 === 0) {
3075
3110
  console.error(`[RelayGateway] ${this.consecutiveFailures} consecutive failures \u2014 check IMAP credentials and connectivity (${this.config?.imapHost}:${this.config?.imapPort})`);
@@ -5326,20 +5361,23 @@ var GatewayManager = class {
5326
5361
  }
5327
5362
  /**
5328
5363
  * Resume gateway from saved config (e.g., after server restart).
5364
+ *
5365
+ * Issue #31 — On a Docker container restart the API can come up
5366
+ * before Stalwart / Gmail IMAP / DNS is reachable, so the very first
5367
+ * setup() can fail with a transient network error. Previously that
5368
+ * single failure was logged and never retried, leaving polling
5369
+ * permanently dead until someone noticed and manually revived the
5370
+ * relay. We now schedule background retries with exponential backoff
5371
+ * (5s, 10s, 20s, 40s, 60s cap, indefinite) so the relay
5372
+ * self-recovers as soon as the dependency is reachable again.
5329
5373
  */
5330
5374
  async resume() {
5331
5375
  if (this.config.mode === "relay" && this.config.relay) {
5332
5376
  try {
5333
- await this.relay.setup(this.config.relay);
5334
- const savedUid = this.loadLastSeenUid();
5335
- if (savedUid > 0) {
5336
- this.relay.setLastSeenUid(savedUid);
5337
- console.log(`[GatewayManager] Restored lastSeenUid=${savedUid} from database`);
5338
- }
5339
- this.relay.onUidAdvance = (uid) => this.saveLastSeenUid(uid);
5340
- await this.relay.startPolling();
5377
+ await this._resumeRelayOnce();
5341
5378
  } catch (err) {
5342
- console.error("[GatewayManager] Failed to resume relay:", err);
5379
+ console.error("[GatewayManager] Initial relay resume failed; scheduling retries:", formatPollError(err));
5380
+ this._scheduleRelayResumeRetry();
5343
5381
  }
5344
5382
  }
5345
5383
  if (this.smsManager && this.accountManager) {
@@ -5366,6 +5404,42 @@ var GatewayManager = class {
5366
5404
  }
5367
5405
  }
5368
5406
  }
5407
+ // ─── Issue #31 helpers — resume retry with backoff ───
5408
+ _resumeRetryTimer = null;
5409
+ _resumeRetryAttempt = 0;
5410
+ async _resumeRelayOnce() {
5411
+ if (!this.config.relay) throw new Error("No relay config to resume");
5412
+ await this.relay.setup(this.config.relay);
5413
+ const savedUid = this.loadLastSeenUid();
5414
+ if (savedUid > 0) {
5415
+ this.relay.setLastSeenUid(savedUid);
5416
+ console.log(`[GatewayManager] Restored lastSeenUid=${savedUid} from database`);
5417
+ }
5418
+ this.relay.onUidAdvance = (uid) => this.saveLastSeenUid(uid);
5419
+ await this.relay.startPolling();
5420
+ if (this._resumeRetryAttempt > 0) {
5421
+ console.log(`[GatewayManager] Relay polling resumed after ${this._resumeRetryAttempt} retry attempt${this._resumeRetryAttempt !== 1 ? "s" : ""}`);
5422
+ }
5423
+ this._resumeRetryAttempt = 0;
5424
+ }
5425
+ _scheduleRelayResumeRetry() {
5426
+ if (this._resumeRetryTimer) return;
5427
+ this._resumeRetryAttempt++;
5428
+ const base = Math.min(5e3 * Math.pow(2, this._resumeRetryAttempt - 1), 6e4);
5429
+ const jitter = base * (0.8 + Math.random() * 0.4);
5430
+ const delay = Math.round(jitter);
5431
+ console.log(`[GatewayManager] Will retry relay resume in ${(delay / 1e3).toFixed(1)}s (attempt ${this._resumeRetryAttempt + 1})`);
5432
+ this._resumeRetryTimer = setTimeout(async () => {
5433
+ this._resumeRetryTimer = null;
5434
+ if (this.config.mode !== "relay" || !this.config.relay) return;
5435
+ try {
5436
+ await this._resumeRelayOnce();
5437
+ } catch (err) {
5438
+ console.error(`[GatewayManager] Relay resume retry ${this._resumeRetryAttempt} failed:`, formatPollError(err));
5439
+ this._scheduleRelayResumeRetry();
5440
+ }
5441
+ }, delay);
5442
+ }
5369
5443
  // --- Persistence ---
5370
5444
  loadConfig() {
5371
5445
  const row = this.db.prepare("SELECT * FROM gateway_config WHERE id = ?").get("default");
@@ -6318,6 +6392,7 @@ import { execFileSync as execFileSync3, execSync as execSync2 } from "child_proc
6318
6392
  import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync4, unlinkSync, mkdirSync as mkdirSync5, chmodSync } from "fs";
6319
6393
  import { join as join8 } from "path";
6320
6394
  import { homedir as homedir7, platform as platform4 } from "os";
6395
+ import { createRequire } from "module";
6321
6396
  var PLIST_LABEL = "com.agenticmail.server";
6322
6397
  var SYSTEMD_UNIT = "agenticmail.service";
6323
6398
  var ServiceManager = class {
@@ -6344,42 +6419,59 @@ var ServiceManager = class {
6344
6419
  }
6345
6420
  /**
6346
6421
  * Find the API server entry point.
6347
- * Searches common locations where agenticmail is installed.
6422
+ *
6423
+ * Issue #26 — Robust path resolution.
6424
+ *
6425
+ * The original implementation hard-coded `node_modules/agenticmail` (the
6426
+ * old unscoped package name). After the rename to `@agenticmail/cli`, that
6427
+ * directory no longer exists, so the resolver fell back to the stale
6428
+ * `~/.agenticmail/api-entry.path` cache and the launchd plist kept pointing
6429
+ * at a deleted path — causing the boot crash loop reported in #26.
6430
+ *
6431
+ * We now prefer `require.resolve('@agenticmail/api')` so the resolution
6432
+ * follows the actual installed location regardless of npm prefix, the
6433
+ * scoped vs unscoped package name, or the package manager (npm global,
6434
+ * pnpm, yarn global, local node_modules). Cached paths are always
6435
+ * validated against the filesystem before being returned.
6348
6436
  */
6349
6437
  getApiEntryPath() {
6350
- const searchDirs = [
6351
- // Global npm install
6352
- join8(homedir7(), "node_modules", "agenticmail"),
6353
- // npx cache / global prefix
6354
- ...(() => {
6355
- try {
6356
- const prefix = execSync2("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6357
- return [
6358
- join8(prefix, "lib", "node_modules", "agenticmail"),
6359
- join8(prefix, "node_modules", "agenticmail")
6360
- ];
6361
- } catch {
6362
- return [];
6363
- }
6364
- })(),
6365
- // Homebrew on macOS
6366
- "/opt/homebrew/lib/node_modules/agenticmail",
6367
- "/usr/local/lib/node_modules/agenticmail"
6438
+ try {
6439
+ const req = createRequire(import.meta.url);
6440
+ const resolved = req.resolve("@agenticmail/api");
6441
+ if (existsSync6(resolved)) return resolved;
6442
+ } catch {
6443
+ }
6444
+ const parentPackages = [
6445
+ join8("@agenticmail", "cli"),
6446
+ // current scoped package
6447
+ "agenticmail"
6448
+ // legacy unscoped package
6368
6449
  ];
6369
- for (const base of searchDirs) {
6370
- const apiPaths = [
6371
- join8(base, "node_modules", "@agenticmail", "api", "dist", "index.js"),
6372
- join8(base, "..", "@agenticmail", "api", "dist", "index.js")
6373
- ];
6374
- for (const p of apiPaths) {
6375
- if (existsSync6(p)) return p;
6450
+ const baseDirs = [
6451
+ // user-local install
6452
+ join8(homedir7(), "node_modules")
6453
+ ];
6454
+ try {
6455
+ const prefix = execSync2("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6456
+ baseDirs.push(join8(prefix, "lib", "node_modules"));
6457
+ baseDirs.push(join8(prefix, "node_modules"));
6458
+ } catch {
6459
+ }
6460
+ baseDirs.push("/opt/homebrew/lib/node_modules");
6461
+ baseDirs.push("/usr/local/lib/node_modules");
6462
+ for (const base of baseDirs) {
6463
+ const sibling = join8(base, "@agenticmail", "api", "dist", "index.js");
6464
+ if (existsSync6(sibling)) return sibling;
6465
+ for (const parent of parentPackages) {
6466
+ const nested = join8(base, parent, "node_modules", "@agenticmail", "api", "dist", "index.js");
6467
+ if (existsSync6(nested)) return nested;
6376
6468
  }
6377
6469
  }
6378
6470
  const dataDir = join8(homedir7(), ".agenticmail");
6379
6471
  const entryCache = join8(dataDir, "api-entry.path");
6380
6472
  if (existsSync6(entryCache)) {
6381
6473
  const cached = readFileSync3(entryCache, "utf-8").trim();
6382
- if (existsSync6(cached)) return cached;
6474
+ if (cached && existsSync6(cached)) return cached;
6383
6475
  }
6384
6476
  throw new Error("Could not find @agenticmail/api entry point. Run `agenticmail start` first to populate the cache.");
6385
6477
  }
@@ -6393,25 +6485,49 @@ var ServiceManager = class {
6393
6485
  }
6394
6486
  /**
6395
6487
  * Get the current package version.
6488
+ *
6489
+ * Issue #26 — resolve the CLI package.json via require.resolve so the
6490
+ * version reflects the *currently installed* @agenticmail/cli, not a
6491
+ * leftover unscoped `agenticmail` package directory.
6396
6492
  */
6397
6493
  getVersion() {
6398
6494
  try {
6399
- const pkgPaths = [
6400
- join8(homedir7(), "node_modules", "agenticmail", "package.json"),
6401
- join8(homedir7(), ".agenticmail", "package-version.json")
6402
- ];
6403
- try {
6404
- const prefix = execSync2("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6405
- pkgPaths.push(join8(prefix, "lib", "node_modules", "agenticmail", "package.json"));
6406
- } catch {
6495
+ const req = createRequire(import.meta.url);
6496
+ const pkgJson = req.resolve("@agenticmail/cli/package.json");
6497
+ if (existsSync6(pkgJson)) {
6498
+ const pkg = JSON.parse(readFileSync3(pkgJson, "utf-8"));
6499
+ if (pkg.version) return pkg.version;
6500
+ }
6501
+ } catch {
6502
+ }
6503
+ try {
6504
+ const apiEntry = this.getApiEntryPath();
6505
+ const apiPkg = join8(apiEntry, "..", "..", "package.json");
6506
+ if (existsSync6(apiPkg)) {
6507
+ const pkg = JSON.parse(readFileSync3(apiPkg, "utf-8"));
6508
+ if (pkg.version) return pkg.version;
6407
6509
  }
6408
- for (const p of pkgPaths) {
6510
+ } catch {
6511
+ }
6512
+ const candidates = [
6513
+ join8(homedir7(), "node_modules", "@agenticmail", "cli", "package.json"),
6514
+ join8(homedir7(), "node_modules", "agenticmail", "package.json"),
6515
+ join8(homedir7(), ".agenticmail", "package-version.json")
6516
+ ];
6517
+ try {
6518
+ const prefix = execSync2("npm prefix -g", { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
6519
+ candidates.push(join8(prefix, "lib", "node_modules", "@agenticmail", "cli", "package.json"));
6520
+ candidates.push(join8(prefix, "lib", "node_modules", "agenticmail", "package.json"));
6521
+ } catch {
6522
+ }
6523
+ for (const p of candidates) {
6524
+ try {
6409
6525
  if (existsSync6(p)) {
6410
6526
  const pkg = JSON.parse(readFileSync3(p, "utf-8"));
6411
6527
  if (pkg.version) return pkg.version;
6412
6528
  }
6529
+ } catch {
6413
6530
  }
6414
- } catch {
6415
6531
  }
6416
6532
  return "unknown";
6417
6533
  }
@@ -6717,6 +6833,86 @@ WantedBy=default.target
6717
6833
  this.uninstall();
6718
6834
  return this.install();
6719
6835
  }
6836
+ /**
6837
+ * Issue #26 — Detect a stale service installation.
6838
+ *
6839
+ * Background: when a user upgrades from the old unscoped `agenticmail`
6840
+ * package to the new `@agenticmail/cli` scoped package, the old
6841
+ * ~/Library/LaunchAgents/com.agenticmail.server.plist and
6842
+ * ~/.agenticmail/bin/start-server.sh files keep pointing at
6843
+ * /opt/homebrew/lib/node_modules/agenticmail/... — a path that no longer
6844
+ * exists post-rename. The result is a launchd crash loop.
6845
+ *
6846
+ * `needsRepair()` returns a non-null reason whenever:
6847
+ * - the service file exists but the start-server.sh it launches is
6848
+ * missing or references a node_modules path that no longer resolves;
6849
+ * - the embedded service version drifts from the running CLI version
6850
+ * (so service files get refreshed on every upgrade — including
6851
+ * in-place version bumps that don't change the install path);
6852
+ * - the cached API entry path no longer exists on disk.
6853
+ *
6854
+ * Returns null when everything checks out — callers should treat that as
6855
+ * "no action needed".
6856
+ *
6857
+ * Platform-aware: only inspects launchd artefacts on darwin and systemd
6858
+ * artefacts on linux. Returns null on unsupported platforms so this can
6859
+ * be called unconditionally from the CLI's start path.
6860
+ */
6861
+ needsRepair() {
6862
+ if (this.os !== "darwin" && this.os !== "linux") return null;
6863
+ const servicePath = this.getServicePath();
6864
+ if (!existsSync6(servicePath)) return null;
6865
+ let serviceContent = "";
6866
+ try {
6867
+ serviceContent = readFileSync3(servicePath, "utf-8");
6868
+ } catch {
6869
+ return { reason: "Service file unreadable" };
6870
+ }
6871
+ const startScript = join8(homedir7(), ".agenticmail", "bin", "start-server.sh");
6872
+ if (serviceContent.includes(startScript)) {
6873
+ if (!existsSync6(startScript)) {
6874
+ return { reason: "start-server.sh is missing" };
6875
+ }
6876
+ let scriptContent = "";
6877
+ try {
6878
+ scriptContent = readFileSync3(startScript, "utf-8");
6879
+ } catch {
6880
+ return { reason: "start-server.sh unreadable" };
6881
+ }
6882
+ const apiPathMatch = scriptContent.match(/(\/[^"\s]+@agenticmail\/api\/dist\/index\.js)/);
6883
+ if (apiPathMatch) {
6884
+ const referenced = apiPathMatch[1];
6885
+ if (!existsSync6(referenced)) {
6886
+ return { reason: `start-server.sh references missing path: ${referenced}` };
6887
+ }
6888
+ }
6889
+ if (/node_modules\/agenticmail\/(?!.*@agenticmail\/cli)/.test(scriptContent)) {
6890
+ const stale = /(\S*node_modules\/agenticmail\/\S*)/.exec(scriptContent)?.[1];
6891
+ if (stale && !existsSync6(stale)) {
6892
+ return { reason: `start-server.sh references legacy unscoped path: ${stale}` };
6893
+ }
6894
+ }
6895
+ } else {
6896
+ return { reason: "Service file does not reference the wrapper script" };
6897
+ }
6898
+ const currentVersion = this.getVersion();
6899
+ if (currentVersion !== "unknown") {
6900
+ if (!serviceContent.includes(`v${currentVersion}`) || !serviceContent.includes(`AGENTICMAIL_SERVICE_VERSION`) || !serviceContent.includes(currentVersion)) {
6901
+ return { reason: `Service version drift (current CLI is v${currentVersion})` };
6902
+ }
6903
+ }
6904
+ const entryCache = join8(homedir7(), ".agenticmail", "api-entry.path");
6905
+ if (existsSync6(entryCache)) {
6906
+ try {
6907
+ const cached = readFileSync3(entryCache, "utf-8").trim();
6908
+ if (cached && !existsSync6(cached)) {
6909
+ return { reason: `Cached API entry path no longer exists: ${cached}` };
6910
+ }
6911
+ } catch {
6912
+ }
6913
+ }
6914
+ return null;
6915
+ }
6720
6916
  };
6721
6917
 
6722
6918
  // src/setup/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.5.58",
3
+ "version": "0.5.61",
4
4
  "description": "Core SDK for AgenticMail — email, SMS, and phone number access for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",