@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 +250 -53
- package/dist/index.d.cts +59 -1
- package/dist/index.d.ts +59 -1
- package/dist/index.js +249 -53
- package/package.json +1 -1
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
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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 =
|
|
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.
|
|
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]
|
|
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
|
-
*
|
|
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
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
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
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
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
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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 =
|
|
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.
|
|
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]
|
|
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
|
-
*
|
|
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
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
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
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
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
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
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
|
-
|
|
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
|