agenticmail 0.3.24 → 0.4.0

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.
Files changed (3) hide show
  1. package/README.md +16 -3
  2. package/dist/cli.js +890 -136
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The main package for [AgenticMail](https://github.com/agenticmail/agenticmail) — email infrastructure for AI agents. This is the package you install to get started.
4
4
 
5
- It bundles a setup wizard, API server launcher, and a full interactive shell with 36 commands for managing agents, sending and receiving email, configuring gateways, and more. It also re-exports everything from `@agenticmail/core` so you can use it as an SDK.
5
+ It bundles a setup wizard, API server launcher, and a full interactive shell with 44 commands for managing agents, sending and receiving email, configuring gateways, and more. It also re-exports everything from `@agenticmail/core` so you can use it as an SDK.
6
6
 
7
7
  ## Install
8
8
 
@@ -42,7 +42,11 @@ Running `agenticmail setup` walks you through everything needed to get email wor
42
42
 
43
43
  3. **Service startup** — starts Docker if needed, ensures Stalwart is running and healthy.
44
44
 
45
- 4. **Email connection** — this is where you choose how your agents connect to the outside world:
45
+ 4. **Email connection** — this is where you choose how your agents connect to the outside world.
46
+
47
+ 5. **Phone number access (optional)** — set up Google Voice for SMS. Agents can receive verification codes and send texts. The wizard validates Gmail/Google Voice email matching, warns about mismatches, and collects separate credentials when needed. SMS reading prioritizes direct Google Voice web access (instant) with email forwarding as fallback.
48
+
49
+ 6. **OpenClaw integration** — if OpenClaw is detected, automatically registers the plugin.
46
50
 
47
51
  ### Relay Mode (Recommended for Getting Started)
48
52
 
@@ -99,7 +103,7 @@ If the server crashes, you get clear error output showing what went wrong.
99
103
 
100
104
  ## The Interactive Shell
101
105
 
102
- The shell is the main way to interact with AgenticMail. It provides 36 commands organized by category, with arrow-key navigation, color-coded output, and keyboard shortcuts.
106
+ The shell is the main way to interact with AgenticMail. It provides 44 commands organized by category, with arrow-key navigation, color-coded output, and keyboard shortcuts.
103
107
 
104
108
  ### Getting Around
105
109
 
@@ -182,8 +186,17 @@ The shell is the main way to interact with AgenticMail. It provides 36 commands
182
186
  |---------|-------------|
183
187
  | `/help` | Show all available commands with descriptions. |
184
188
  | `/clear` | Clear the screen. |
189
+ | `/update` | Check for and install the latest AgenticMail version. Auto-detects OpenClaw and updates both. |
185
190
  | `/exit` | Exit the shell (also `/quit`). Stops the server and cleans up. |
186
191
 
192
+ ### CLI Update Command
193
+
194
+ ```bash
195
+ agenticmail update
196
+ ```
197
+
198
+ Checks npm for the latest version, compares with your current install, and updates in-place. If OpenClaw is detected, it also updates `@agenticmail/openclaw` and restarts the gateway automatically. Works with npm, pnpm, and bun.
199
+
187
200
  ---
188
201
 
189
202
  ## Inbox Navigation
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { createInterface as createInterface2, emitKeypressEvents as emitKeypressEvents2 } from "readline";
5
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, realpathSync } from "fs";
5
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync, realpathSync, unlinkSync } from "fs";
6
6
  import { join, dirname } from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import { createRequire } from "module";
@@ -235,6 +235,21 @@ async function interactiveShell(options) {
235
235
  log(` ${c.dim("Server:")} ${c.cyan(`http://${config.api.host}:${config.api.port}`)}`);
236
236
  if (agentLine) log(` ${c.dim("Agents:")} ${agentLine}`);
237
237
  if (emailLine) log(` ${c.dim("Email:")} ${emailLine}`);
238
+ try {
239
+ const agentsResp = await apiFetch("/api/agenticmail/accounts");
240
+ if (agentsResp.ok) {
241
+ const agentsData = await agentsResp.json();
242
+ const agents = agentsData.agents || agentsData.accounts || [];
243
+ for (const a of agents) {
244
+ const smsConf = a.metadata?.sms;
245
+ if (smsConf?.enabled && smsConf.phoneNumber) {
246
+ log(` ${c.dim("Phone:")} ${c.green(smsConf.phoneNumber)} ${c.dim("via Google Voice")} ${c.dim("(" + a.name + ")")}`);
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ } catch {
252
+ }
238
253
  log("");
239
254
  log("");
240
255
  log(` ${c.dim("Type")} ${c.bold("/help")} ${c.dim("for commands, or")} ${c.bold("/exit")} ${c.dim("to stop.")}`);
@@ -1665,6 +1680,14 @@ ${orig.text || ""}`;
1665
1680
  log(` ${c.cyan("\u{1F4C1}")} ${f.path}${special}`);
1666
1681
  }
1667
1682
  }
1683
+ try {
1684
+ const smsResp = await agentFetch(agent.apiKey, "/api/agenticmail/sms/config");
1685
+ const smsData = await smsResp.json();
1686
+ if (smsData.sms?.enabled) {
1687
+ log(` ${c.green("\u{1F4F1}")} SMS ${c.dim(`(${smsData.sms.phoneNumber})`)}`);
1688
+ }
1689
+ } catch {
1690
+ }
1668
1691
  } else if (choice.trim() === "2" || choice.trim().toLowerCase() === "create") {
1669
1692
  const name = await question(` ${c.dim("Folder name:")} `);
1670
1693
  if (isBack(name) || !name.trim()) {
@@ -1694,10 +1717,25 @@ ${orig.text || ""}`;
1694
1717
  log("");
1695
1718
  return;
1696
1719
  }
1720
+ let hasSms = false;
1721
+ try {
1722
+ const smsResp = await agentFetch(agent.apiKey, "/api/agenticmail/sms/config");
1723
+ const smsData = await smsResp.json();
1724
+ if (smsData.sms?.enabled) hasSms = true;
1725
+ } catch {
1726
+ }
1697
1727
  for (let i = 0; i < folders.length; i++) {
1698
1728
  log(` ${c.dim(`[${i + 1}]`)} ${c.cyan(folders[i].path)}`);
1699
1729
  }
1700
- const idx = await askChoice(` ${c.dim("Folder #:")} `, folders.length);
1730
+ if (hasSms) {
1731
+ log(` ${c.dim(`[${folders.length + 1}]`)} ${c.green("SMS")} ${c.dim("(text messages)")}`);
1732
+ }
1733
+ const totalChoices = hasSms ? folders.length + 1 : folders.length;
1734
+ const idx = await askChoice(` ${c.dim("Folder #:")} `, totalChoices);
1735
+ if (idx !== null && hasSms && idx === folders.length) {
1736
+ await commands.sms.run();
1737
+ return;
1738
+ }
1701
1739
  if (idx === null) {
1702
1740
  log("");
1703
1741
  return;
@@ -3617,6 +3655,308 @@ ${c.dim(boxChar.bl + boxChar.h.repeat(bWidth) + boxChar.br)}`);
3617
3655
  }
3618
3656
  }
3619
3657
  },
3658
+ sms: {
3659
+ desc: "Manage SMS / phone number (view, setup, change, disable)",
3660
+ run: async () => {
3661
+ const agent = getActiveAgent();
3662
+ if (!agent) return;
3663
+ log("");
3664
+ log(hr());
3665
+ heading("SMS / Phone Number");
3666
+ log("");
3667
+ let smsConfig = null;
3668
+ try {
3669
+ const resp = await fetch(`${apiBase}/api/agenticmail/sms/config`, {
3670
+ headers: { "Authorization": `Bearer ${agent.apiKey}` }
3671
+ });
3672
+ const data = await resp.json();
3673
+ smsConfig = data.sms;
3674
+ } catch {
3675
+ }
3676
+ if (smsConfig?.enabled) {
3677
+ log(` ${c.green("\u25CF")} SMS is ${c.green("enabled")}`);
3678
+ log(` ${c.dim("Phone number:")} ${c.bold(smsConfig.phoneNumber)}`);
3679
+ log(` ${c.dim("Forwarding to:")} ${smsConfig.forwardingEmail || c.dim("(agent email)")}`);
3680
+ log(` ${c.dim("Provider:")} ${smsConfig.provider}`);
3681
+ log(` ${c.dim("Configured:")} ${smsConfig.configuredAt ? new Date(smsConfig.configuredAt).toLocaleDateString() : "unknown"}`);
3682
+ } else if (smsConfig && !smsConfig.enabled) {
3683
+ log(` ${c.yellow("\u25CF")} SMS is ${c.yellow("disabled")}`);
3684
+ log(` ${c.dim("Phone number:")} ${smsConfig.phoneNumber}`);
3685
+ } else {
3686
+ log(` ${c.dim("\u25CF")} SMS is ${c.dim("not configured")}`);
3687
+ }
3688
+ log("");
3689
+ log(` ${c.bold("Options:")}`);
3690
+ if (!smsConfig?.enabled) {
3691
+ log(` ${c.cyan("1")} Set up a phone number`);
3692
+ } else {
3693
+ log(` ${c.cyan("1")} View SMS messages`);
3694
+ log(` ${c.cyan("2")} Change phone number`);
3695
+ log(` ${c.cyan("3")} Check for verification codes`);
3696
+ log(` ${c.cyan("4")} Disable SMS`);
3697
+ }
3698
+ log(` ${c.dim("Enter")} Go back`);
3699
+ log("");
3700
+ const choice = await new Promise((resolve) => {
3701
+ rl.question(` ${c.bold("Choose:")} `, resolve);
3702
+ });
3703
+ if (!smsConfig?.enabled) {
3704
+ if (choice.trim() === "1") {
3705
+ log("");
3706
+ log(` ${c.bold("Google Voice Setup (takes ~2 minutes):")}`);
3707
+ log("");
3708
+ log(` 1. Go to ${c.cyan("https://voice.google.com")}`);
3709
+ log(` 2. Sign in with your Google account`);
3710
+ log(` 3. Click "Choose a phone number"`);
3711
+ log(` 4. Search by city or area code, pick a number`);
3712
+ log(` 5. Verify with your existing phone`);
3713
+ log(` 6. Go to Settings > Messages > Enable "Forward messages to email"`);
3714
+ log("");
3715
+ const phone = await new Promise((resolve) => {
3716
+ rl.question(` ${c.bold("Google Voice number")} ${c.dim("(e.g. +12125551234):")} `, resolve);
3717
+ });
3718
+ if (!phone.trim()) {
3719
+ info("Cancelled.");
3720
+ return;
3721
+ }
3722
+ const fwdEmail = await new Promise((resolve) => {
3723
+ rl.question(` ${c.bold("Forwarding email")} ${c.dim("(Enter for agent email):")} `, resolve);
3724
+ });
3725
+ try {
3726
+ const resp = await fetch(`${apiBase}/api/agenticmail/sms/setup`, {
3727
+ method: "POST",
3728
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${agent.apiKey}` },
3729
+ body: JSON.stringify({ phoneNumber: phone.trim(), forwardingEmail: fwdEmail.trim() || void 0 })
3730
+ });
3731
+ const data = await resp.json();
3732
+ if (data.success) {
3733
+ ok(`Phone number saved: ${data.sms?.phoneNumber || phone.trim()}`);
3734
+ info("Make sure SMS forwarding is enabled in Google Voice settings.");
3735
+ } else {
3736
+ fail(data.error || "Setup failed");
3737
+ }
3738
+ } catch (err) {
3739
+ fail(err.message);
3740
+ }
3741
+ }
3742
+ } else {
3743
+ if (choice.trim() === "1") {
3744
+ try {
3745
+ const resp = await fetch(`${apiBase}/api/agenticmail/sms/messages?limit=20`, {
3746
+ headers: { "Authorization": `Bearer ${agent.apiKey}` }
3747
+ });
3748
+ const data = await resp.json();
3749
+ if (!data.messages?.length) {
3750
+ info("No SMS messages yet.");
3751
+ } else {
3752
+ log("");
3753
+ for (const msg of data.messages) {
3754
+ const dir = msg.direction === "inbound" ? c.green("\u2190 IN ") : c.blue("\u2192 OUT");
3755
+ const status = msg.status === "received" ? "" : ` ${c.dim(`[${msg.status}]`)}`;
3756
+ const time = new Date(msg.createdAt).toLocaleString();
3757
+ log(` ${dir} ${c.bold(msg.phoneNumber)}${status} ${c.dim(time)}`);
3758
+ log(` ${msg.body.length > 80 ? msg.body.slice(0, 80) + "..." : msg.body}`);
3759
+ log("");
3760
+ }
3761
+ info(`${data.messages.length} message(s)`);
3762
+ }
3763
+ } catch (err) {
3764
+ fail(err.message);
3765
+ }
3766
+ } else if (choice.trim() === "2") {
3767
+ const newPhone = await new Promise((resolve) => {
3768
+ rl.question(` ${c.bold("New Google Voice number")} ${c.dim("(e.g. +12125551234):")} `, resolve);
3769
+ });
3770
+ if (!newPhone.trim()) {
3771
+ info("Cancelled.");
3772
+ return;
3773
+ }
3774
+ const newFwd = await new Promise((resolve) => {
3775
+ rl.question(` ${c.bold("Forwarding email")} ${c.dim("(Enter to keep current):")} `, resolve);
3776
+ });
3777
+ try {
3778
+ const resp = await fetch(`${apiBase}/api/agenticmail/sms/setup`, {
3779
+ method: "POST",
3780
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${agent.apiKey}` },
3781
+ body: JSON.stringify({ phoneNumber: newPhone.trim(), forwardingEmail: newFwd.trim() || void 0 })
3782
+ });
3783
+ const data = await resp.json();
3784
+ if (data.success) {
3785
+ ok(`Phone number updated to: ${data.sms?.phoneNumber || newPhone.trim()}`);
3786
+ } else {
3787
+ fail(data.error || "Update failed");
3788
+ }
3789
+ } catch (err) {
3790
+ fail(err.message);
3791
+ }
3792
+ } else if (choice.trim() === "3") {
3793
+ try {
3794
+ const resp = await fetch(`${apiBase}/api/agenticmail/sms/verification-code?minutes=30`, {
3795
+ headers: { "Authorization": `Bearer ${agent.apiKey}` }
3796
+ });
3797
+ const data = await resp.json();
3798
+ if (data.found) {
3799
+ log("");
3800
+ ok(`Verification code found: ${c.bold(c.green(data.code))}`);
3801
+ log(` ${c.dim("From:")} ${data.from}`);
3802
+ log(` ${c.dim("Message:")} ${data.body}`);
3803
+ log(` ${c.dim("Received:")} ${new Date(data.receivedAt).toLocaleString()}`);
3804
+ } else {
3805
+ info("No verification codes found in the last 30 minutes.");
3806
+ info("Make sure Google Voice SMS forwarding is enabled and use /inbox to check for forwarded SMS emails.");
3807
+ }
3808
+ } catch (err) {
3809
+ fail(err.message);
3810
+ }
3811
+ } else if (choice.trim() === "4") {
3812
+ const confirm = await new Promise((resolve) => {
3813
+ rl.question(` ${c.bold("Disable SMS?")} ${c.dim("This keeps your number saved but stops SMS features. (y/N)")} `, resolve);
3814
+ });
3815
+ if (confirm.toLowerCase().startsWith("y")) {
3816
+ try {
3817
+ const resp = await fetch(`${apiBase}/api/agenticmail/sms/disable`, {
3818
+ method: "POST",
3819
+ headers: { "Authorization": `Bearer ${agent.apiKey}` }
3820
+ });
3821
+ const data = await resp.json();
3822
+ if (data.success) {
3823
+ ok("SMS disabled. Your number is still saved. Use /sms to re-enable anytime.");
3824
+ } else {
3825
+ fail(data.error || "Failed to disable");
3826
+ }
3827
+ } catch (err) {
3828
+ fail(err.message);
3829
+ }
3830
+ } else {
3831
+ info("Cancelled.");
3832
+ }
3833
+ }
3834
+ }
3835
+ log("");
3836
+ }
3837
+ },
3838
+ update: {
3839
+ desc: "Check for and install the latest AgenticMail version",
3840
+ run: async () => {
3841
+ log("");
3842
+ log(hr());
3843
+ heading("Update AgenticMail");
3844
+ log("");
3845
+ const { execSync } = await import("child_process");
3846
+ let currentVersion = "unknown";
3847
+ try {
3848
+ const pkg = await import("agenticmail/package.json");
3849
+ currentVersion = pkg.default?.version ?? "unknown";
3850
+ } catch {
3851
+ try {
3852
+ const { readFileSync: readFileSync3 } = await import("fs");
3853
+ const { join: join2, dirname: dirname2 } = await import("path");
3854
+ const { fileURLToPath: fileURLToPath2 } = await import("url");
3855
+ const thisDir = dirname2(fileURLToPath2(import.meta.url));
3856
+ const pkgPath = join2(thisDir, "..", "package.json");
3857
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
3858
+ currentVersion = pkg.version ?? "unknown";
3859
+ } catch {
3860
+ }
3861
+ }
3862
+ info(`Current version: ${c.bold(currentVersion)}`);
3863
+ let latestVersion = "unknown";
3864
+ try {
3865
+ latestVersion = execSync("npm view agenticmail version", { encoding: "utf-8", timeout: 15e3 }).trim();
3866
+ } catch {
3867
+ fail("Could not check npm for latest version. Check your internet connection.");
3868
+ return;
3869
+ }
3870
+ info(`Latest version: ${c.bold(latestVersion)}`);
3871
+ if (currentVersion === latestVersion) {
3872
+ ok("You are already on the latest version!");
3873
+ log("");
3874
+ return;
3875
+ }
3876
+ log("");
3877
+ info(`New version available: ${c.yellow(currentVersion)} \u2192 ${c.green(latestVersion)}`);
3878
+ let hasOpenClaw = false;
3879
+ let openClawVersion = "";
3880
+ try {
3881
+ openClawVersion = execSync('openclaw --version 2>/dev/null || echo ""', { encoding: "utf-8", timeout: 1e4 }).trim();
3882
+ if (openClawVersion) hasOpenClaw = true;
3883
+ } catch {
3884
+ }
3885
+ if (hasOpenClaw) {
3886
+ info(`OpenClaw detected: ${c.bold(openClawVersion)}`);
3887
+ try {
3888
+ const peerDeps = execSync(`npm view agenticmail@${latestVersion} peerDependencies --json 2>/dev/null`, { encoding: "utf-8", timeout: 15e3 }).trim();
3889
+ if (peerDeps) {
3890
+ const deps = JSON.parse(peerDeps);
3891
+ if (deps.openclaw) {
3892
+ info(`Required OpenClaw version: ${c.bold(deps.openclaw)}`);
3893
+ }
3894
+ }
3895
+ } catch {
3896
+ }
3897
+ info("OpenClaw plugin will also be updated.");
3898
+ }
3899
+ log("");
3900
+ const confirm = await new Promise((resolve) => {
3901
+ rl.question(` ${c.bold("Update now?")} ${c.dim("(Y/n)")} `, resolve);
3902
+ });
3903
+ if (confirm.toLowerCase().startsWith("n")) {
3904
+ info("Update cancelled.");
3905
+ log("");
3906
+ return;
3907
+ }
3908
+ log("");
3909
+ info("Updating...");
3910
+ try {
3911
+ let pm = "npm";
3912
+ try {
3913
+ execSync("pnpm --version", { stdio: "ignore", timeout: 5e3 });
3914
+ pm = "pnpm";
3915
+ } catch {
3916
+ try {
3917
+ execSync("bun --version", { stdio: "ignore", timeout: 5e3 });
3918
+ pm = "bun";
3919
+ } catch {
3920
+ }
3921
+ }
3922
+ let isGlobal = false;
3923
+ try {
3924
+ const globalList = execSync(`${pm === "npm" ? "npm" : pm} list -g agenticmail 2>/dev/null`, { encoding: "utf-8", timeout: 1e4 });
3925
+ if (globalList.includes("agenticmail")) isGlobal = true;
3926
+ } catch {
3927
+ }
3928
+ const scope = isGlobal ? "-g" : "";
3929
+ const installCmd = pm === "bun" ? `bun add ${scope} agenticmail@latest` : `${pm} install ${scope} agenticmail@latest`;
3930
+ info(`Running: ${c.dim(installCmd)}`);
3931
+ execSync(installCmd, { stdio: "inherit", timeout: 12e4 });
3932
+ if (hasOpenClaw) {
3933
+ const pluginCmd = pm === "bun" ? `bun add ${scope} @agenticmail/openclaw@latest` : `${pm} install ${scope} @agenticmail/openclaw@latest`;
3934
+ info(`Updating OpenClaw plugin: ${c.dim(pluginCmd)}`);
3935
+ try {
3936
+ execSync(pluginCmd, { stdio: "inherit", timeout: 12e4 });
3937
+ ok("OpenClaw plugin updated.");
3938
+ try {
3939
+ execSync("openclaw gateway restart", { stdio: "pipe", timeout: 3e4 });
3940
+ ok("OpenClaw gateway restarted.");
3941
+ } catch {
3942
+ info(`Restart OpenClaw manually: ${c.green("openclaw gateway restart")}`);
3943
+ }
3944
+ } catch (err) {
3945
+ log(` ${c.yellow("!")} Plugin update failed: ${err.message}`);
3946
+ info(`Update manually: ${c.green(pluginCmd)}`);
3947
+ }
3948
+ }
3949
+ log("");
3950
+ ok(`Updated to agenticmail@${latestVersion}`);
3951
+ info("Restart the shell to use the new version.");
3952
+ log("");
3953
+ } catch (err) {
3954
+ fail(`Update failed: ${err.message}`);
3955
+ info(`Try manually: ${c.green("npm install -g agenticmail@latest")}`);
3956
+ log("");
3957
+ }
3958
+ }
3959
+ },
3620
3960
  exit: {
3621
3961
  desc: "Stop the server and exit",
3622
3962
  run: async () => {
@@ -4054,22 +4394,53 @@ async function waitForApi(host, port, timeoutMs = 15e3) {
4054
4394
  }
4055
4395
  return false;
4056
4396
  }
4057
- var apiChild = null;
4058
- function cleanupChild() {
4059
- if (apiChild) {
4060
- apiChild.kill();
4061
- apiChild = null;
4397
+ var PID_FILE = join(homedir(), ".agenticmail", "server.pid");
4398
+ async function startApiServer(config) {
4399
+ const host = config.api.host;
4400
+ const port = config.api.port;
4401
+ try {
4402
+ const probe = await fetch(`http://${host}:${port}/api/agenticmail/health`, {
4403
+ signal: AbortSignal.timeout(2e3)
4404
+ });
4405
+ if (probe.ok) return true;
4406
+ } catch {
4407
+ }
4408
+ const { spawn } = await import("child_process");
4409
+ const apiEntry = resolveApiEntry();
4410
+ const env = configToEnv(config);
4411
+ const child = spawn(process.execPath, [apiEntry], {
4412
+ detached: true,
4413
+ stdio: "ignore",
4414
+ env
4415
+ });
4416
+ child.unref();
4417
+ if (child.pid) {
4418
+ try {
4419
+ writeFileSync2(PID_FILE, String(child.pid));
4420
+ } catch {
4421
+ }
4422
+ }
4423
+ return waitForApi(host, port);
4424
+ }
4425
+ function stopApiServer() {
4426
+ try {
4427
+ if (!existsSync2(PID_FILE)) return false;
4428
+ const pid = parseInt(readFileSync2(PID_FILE, "utf-8").trim(), 10);
4429
+ if (isNaN(pid)) return false;
4430
+ process.kill(pid, "SIGTERM");
4431
+ try {
4432
+ unlinkSync(PID_FILE);
4433
+ } catch {
4434
+ }
4435
+ return true;
4436
+ } catch {
4437
+ try {
4438
+ unlinkSync(PID_FILE);
4439
+ } catch {
4440
+ }
4441
+ return false;
4062
4442
  }
4063
4443
  }
4064
- process.on("exit", cleanupChild);
4065
- process.on("SIGINT", () => {
4066
- cleanupChild();
4067
- process.exit(0);
4068
- });
4069
- process.on("SIGTERM", () => {
4070
- cleanupChild();
4071
- process.exit(0);
4072
- });
4073
4444
  async function cmdSetup() {
4074
4445
  log2("");
4075
4446
  log2(` ${c2.bgCyan(" AgenticMail Setup ")}`);
@@ -4078,7 +4449,7 @@ async function cmdSetup() {
4078
4449
  log2(` needs to send and receive real email.`);
4079
4450
  log2("");
4080
4451
  const hasOpenClaw = existsSync2(join(homedir(), ".openclaw", "openclaw.json"));
4081
- const totalSteps = hasOpenClaw ? 5 : 4;
4452
+ const totalSteps = hasOpenClaw ? 6 : 5;
4082
4453
  log2(` Here's what we'll do:`);
4083
4454
  log2(` ${c2.dim("1.")} Check your system for required tools`);
4084
4455
  log2(` ${c2.dim("2.")} Create your private account and keys`);
@@ -4258,51 +4629,14 @@ async function cmdSetup() {
4258
4629
  serverSpinner.start();
4259
4630
  let serverReady = false;
4260
4631
  try {
4261
- const probe = await fetch(`http://${result.config.api.host}:${result.config.api.port}/api/agenticmail/health`, {
4262
- signal: AbortSignal.timeout(2e3)
4263
- });
4264
- if (probe.ok) serverReady = true;
4265
- } catch {
4266
- }
4267
- if (serverReady) {
4268
- serverSpinner.succeed(`Server already running at ${c2.cyan(`http://${result.config.api.host}:${result.config.api.port}`)}`);
4269
- } else {
4270
- try {
4271
- const { fork } = await import("child_process");
4272
- const apiEntry = resolveApiEntry();
4273
- const env = configToEnv(result.config);
4274
- apiChild = fork(apiEntry, [], { stdio: ["ignore", "ignore", "pipe", "ipc"], env });
4275
- const stderrLines = [];
4276
- apiChild.stderr?.on("data", (chunk) => {
4277
- const lines = chunk.toString().trim().split("\n");
4278
- for (const line of lines) {
4279
- stderrLines.push(line);
4280
- if (stderrLines.length > 50) stderrLines.shift();
4281
- }
4282
- });
4283
- apiChild.on("exit", (code, signal) => {
4284
- apiChild = null;
4285
- log2("");
4286
- fail2(`Server stopped unexpectedly${signal ? ` (signal: ${signal})` : code ? ` (exit code: ${code})` : ""}`);
4287
- if (stderrLines.length > 0) {
4288
- log2("");
4289
- log2(` ${c2.dim("Last server output:")}`);
4290
- for (const line of stderrLines.slice(-10)) {
4291
- log2(` ${c2.dim(line)}`);
4292
- }
4293
- }
4294
- log2("");
4295
- process.exit(code ?? 1);
4296
- });
4297
- serverReady = await waitForApi(result.config.api.host, result.config.api.port);
4298
- if (serverReady) {
4299
- serverSpinner.succeed(`Server running at ${c2.cyan(`http://${result.config.api.host}:${result.config.api.port}`)}`);
4300
- } else {
4301
- serverSpinner.fail("Server did not start in time");
4302
- }
4303
- } catch (err) {
4304
- serverSpinner.fail(`Could not start server: ${err.message}`);
4632
+ serverReady = await startApiServer(result.config);
4633
+ if (serverReady) {
4634
+ serverSpinner.succeed(`Server running at ${c2.cyan(`http://${result.config.api.host}:${result.config.api.port}`)}`);
4635
+ } else {
4636
+ serverSpinner.fail("Server did not start in time");
4305
4637
  }
4638
+ } catch (err) {
4639
+ serverSpinner.fail(`Could not start server: ${err.message}`);
4306
4640
  }
4307
4641
  let existingEmail = null;
4308
4642
  let existingProvider = null;
@@ -4363,7 +4697,6 @@ async function cmdSetup() {
4363
4697
  if (choice === "1" || choice === "2") {
4364
4698
  if (!serverReady) {
4365
4699
  info2("You can configure email later by running: agenticmail setup");
4366
- cleanupChild();
4367
4700
  printSummary(result, true);
4368
4701
  return;
4369
4702
  }
@@ -4378,22 +4711,130 @@ async function cmdSetup() {
4378
4711
  if (!emailOk) {
4379
4712
  log2("");
4380
4713
  info2("Email setup did not complete. Run " + c2.green("npx agenticmail setup") + " again to retry.");
4381
- cleanupChild();
4382
4714
  printSummary(result, true);
4383
4715
  return;
4384
4716
  }
4385
4717
  } else if (!existingEmail) {
4386
4718
  info2("No problem! You can set up email anytime by running this again.");
4387
4719
  }
4720
+ if (serverReady) {
4721
+ log2("");
4722
+ log2(` ${c2.bold(`Step 5 of ${totalSteps}`)} ${c2.dim("\u2014")} ${c2.bold("Phone number access (optional)")}`);
4723
+ log2("");
4724
+ log2(` ${c2.dim("Give your AI agent a phone number via Google Voice.")}`);
4725
+ log2(` ${c2.dim("This lets agents receive verification codes and send texts.")}`);
4726
+ log2("");
4727
+ const wantSms = await ask(` ${c2.bold("Set up phone number access?")} ${c2.dim("(y/N)")} `);
4728
+ if (wantSms.toLowerCase().startsWith("y")) {
4729
+ log2("");
4730
+ log2(` ${c2.bold("What this does:")}`);
4731
+ log2(` Your AI agent gets a real phone number it can use to:`);
4732
+ log2(` ${c2.dim("*")} Receive verification codes when signing up for services`);
4733
+ log2(` ${c2.dim("*")} Send and receive text messages`);
4734
+ log2(` ${c2.dim("*")} Verify accounts on platforms that require phone numbers`);
4735
+ log2("");
4736
+ log2(` ${c2.bold("How it works:")}`);
4737
+ log2(` Google Voice gives you a free US phone number. When someone`);
4738
+ log2(` texts that number, Google forwards it to your email. Your`);
4739
+ log2(` agent reads the email and extracts the message or code.`);
4740
+ log2("");
4741
+ const hasVoice = await ask(` ${c2.bold("Do you already have a Google Voice number?")} ${c2.dim("(y/N)")} `);
4742
+ if (!hasVoice.toLowerCase().startsWith("y")) {
4743
+ log2("");
4744
+ log2(` ${c2.bold("No problem! Setting up Google Voice takes about 2 minutes:")}`);
4745
+ log2("");
4746
+ log2(` ${c2.cyan("Step 1:")} Open ${c2.bold(c2.cyan("https://voice.google.com"))} in your browser`);
4747
+ log2(` ${c2.cyan("Step 2:")} Sign in with your Google account`);
4748
+ log2(` ${c2.cyan("Step 3:")} Click ${c2.bold('"Choose a phone number"')}`);
4749
+ log2(` ${c2.cyan("Step 4:")} Search for a number by city or area code`);
4750
+ log2(` ${c2.cyan("Step 5:")} Pick a number and click ${c2.bold('"Verify"')}`);
4751
+ log2(` ${c2.dim("(Google will verify via your existing phone number)")}`);
4752
+ log2(` ${c2.cyan("Step 6:")} Once verified, go to ${c2.bold("Settings")} (gear icon)`);
4753
+ log2(` ${c2.cyan("Step 7:")} Under Messages, enable ${c2.bold('"Forward messages to email"')}`);
4754
+ log2("");
4755
+ log2(` ${c2.dim("That's it! Come back here when you have your number.")}`);
4756
+ log2("");
4757
+ const ready = await ask(` ${c2.bold("Press Enter when you have your Google Voice number ready...")} `);
4758
+ }
4759
+ log2("");
4760
+ const phoneNumber = await ask(` ${c2.bold("Your Google Voice phone number")} ${c2.dim("(e.g. +12125551234):")} `);
4761
+ if (phoneNumber.trim()) {
4762
+ const digits = phoneNumber.replace(/[^+\d]/g, "").replace(/\D/g, "");
4763
+ if (digits.length < 10) {
4764
+ log2(` ${c2.yellow("!")} That doesn't look like a valid phone number (need at least 10 digits).`);
4765
+ info2("You can set this up later in the shell with /sms or via the agenticmail_sms_setup tool.");
4766
+ } else {
4767
+ const forwardEmail = await ask(` ${c2.bold("Email Google Voice forwards SMS to")} ${c2.dim("(Enter to use agent email):")} `);
4768
+ try {
4769
+ const apiBase = `http://${result.config.api.host}:${result.config.api.port}`;
4770
+ const resp = await fetch(`${apiBase}/api/agenticmail/sms/setup`, {
4771
+ method: "POST",
4772
+ headers: {
4773
+ "Content-Type": "application/json",
4774
+ "Authorization": `Bearer ${result.config.masterKey}`
4775
+ },
4776
+ body: JSON.stringify({
4777
+ phoneNumber: phoneNumber.trim(),
4778
+ forwardingEmail: forwardEmail.trim() || void 0
4779
+ })
4780
+ });
4781
+ const data = await resp.json();
4782
+ if (data.success) {
4783
+ log2("");
4784
+ log2(` ${c2.green("\u2714")} Phone number saved: ${c2.bold(data.sms?.phoneNumber || phoneNumber.trim())}`);
4785
+ log2("");
4786
+ log2(` ${c2.bold("Important:")} Make sure you enabled SMS forwarding in Google Voice:`);
4787
+ log2(` ${c2.dim("voice.google.com > Settings > Messages > Forward messages to email")}`);
4788
+ log2("");
4789
+ log2(` ${c2.dim("Your agent can now receive verification codes and text messages.")}`);
4790
+ log2(` ${c2.dim("Manage SMS anytime in the shell with /sms")}`);
4791
+ } else {
4792
+ throw new Error(data.error || "API call failed");
4793
+ }
4794
+ } catch {
4795
+ try {
4796
+ const { readFileSync: readFileSync3, writeFileSync: writeFileSync3 } = await import("fs");
4797
+ const { join: join2 } = await import("path");
4798
+ const os = await import("os");
4799
+ const configPath = join2(result.config.dataDir || os.homedir() + "/.agenticmail", "config.json");
4800
+ let fileConfig = {};
4801
+ try {
4802
+ fileConfig = JSON.parse(readFileSync3(configPath, "utf-8"));
4803
+ } catch {
4804
+ }
4805
+ fileConfig.sms = {
4806
+ enabled: true,
4807
+ phoneNumber: phoneNumber.trim(),
4808
+ forwardingEmail: forwardEmail.trim() || "",
4809
+ provider: "google_voice",
4810
+ configuredAt: (/* @__PURE__ */ new Date()).toISOString()
4811
+ };
4812
+ writeFileSync3(configPath, JSON.stringify(fileConfig, null, 2), { encoding: "utf-8", mode: 384 });
4813
+ log2(` ${c2.green("\u2714")} Phone number saved: ${c2.bold(phoneNumber.trim())}`);
4814
+ log2(` ${c2.dim("Make sure SMS forwarding is enabled in Google Voice settings.")}`);
4815
+ } catch (err) {
4816
+ log2(` ${c2.yellow("!")} Could not save: ${err.message}`);
4817
+ log2(` ${c2.dim("Set up later in the shell with /sms")}`);
4818
+ }
4819
+ }
4820
+ }
4821
+ } else {
4822
+ info2("Skipped. Set up anytime in the shell with /sms");
4823
+ }
4824
+ } else {
4825
+ info2("Skipped. Add a phone number anytime with /sms in the shell.");
4826
+ }
4827
+ }
4388
4828
  if (hasOpenClaw && serverReady) {
4389
4829
  log2("");
4390
- log2(` ${c2.bold(`Step 5 of ${totalSteps}`)} ${c2.dim("\u2014")} ${c2.bold("Configure OpenClaw integration")}`);
4830
+ log2(` ${c2.bold(`Step 6 of ${totalSteps}`)} ${c2.dim("\u2014")} ${c2.bold("Configure OpenClaw integration")}`);
4391
4831
  log2("");
4392
4832
  await registerWithOpenClaw(result.config);
4393
4833
  }
4394
4834
  printSummary(result, false);
4395
4835
  if (serverReady) {
4396
- await interactiveShell({ config: result.config, onExit: cleanupChild });
4836
+ await interactiveShell({ config: result.config, onExit: () => {
4837
+ } });
4397
4838
  }
4398
4839
  }
4399
4840
  function printSummary(result, exitAfter) {
@@ -4975,11 +5416,12 @@ async function cmdOpenClaw() {
4975
5416
  log2(` This will:`);
4976
5417
  log2(` ${c2.dim("1.")} Set up the mail server infrastructure`);
4977
5418
  log2(` ${c2.dim("2.")} Create an agent email account`);
4978
- log2(` ${c2.dim("3.")} Configure the OpenClaw plugin`);
4979
- log2(` ${c2.dim("4.")} Restart the OpenClaw gateway`);
5419
+ log2(` ${c2.dim("3.")} Set up phone number access ${c2.green("NEW")}`);
5420
+ log2(` ${c2.dim("4.")} Configure the OpenClaw plugin`);
5421
+ log2(` ${c2.dim("5.")} Restart the OpenClaw gateway`);
4980
5422
  log2("");
4981
5423
  const setup = new SetupManager();
4982
- log2(` ${c2.bold("Step 1 of 5")} ${c2.dim("\u2014")} ${c2.bold("Checking infrastructure")}`);
5424
+ log2(` ${c2.bold("Step 1 of 6")} ${c2.dim("\u2014")} ${c2.bold("Checking infrastructure")}`);
4983
5425
  log2("");
4984
5426
  let config;
4985
5427
  let configPath;
@@ -5038,7 +5480,7 @@ async function cmdOpenClaw() {
5038
5480
  }
5039
5481
  }
5040
5482
  log2("");
5041
- log2(` ${c2.bold("Step 2 of 5")} ${c2.dim("\u2014")} ${c2.bold("Starting server")}`);
5483
+ log2(` ${c2.bold("Step 2 of 6")} ${c2.dim("\u2014")} ${c2.bold("Starting server")}`);
5042
5484
  log2("");
5043
5485
  const apiHost = config.api.host;
5044
5486
  const apiPort = config.api.port;
@@ -5057,17 +5499,9 @@ async function cmdOpenClaw() {
5057
5499
  const serverSpinner = new Spinner("server", "Starting the server...");
5058
5500
  serverSpinner.start();
5059
5501
  try {
5060
- const { fork } = await import("child_process");
5061
- const apiEntry = resolveApiEntry();
5062
- const env = configToEnv(config);
5063
- apiChild = fork(apiEntry, [], { stdio: ["ignore", "ignore", "pipe", "ipc"], env });
5064
- apiChild.on("exit", () => {
5065
- apiChild = null;
5066
- });
5067
- const ready = await waitForApi(apiHost, apiPort);
5502
+ const ready = await startApiServer(config);
5068
5503
  if (!ready) {
5069
5504
  serverSpinner.fail("Server did not start in time");
5070
- cleanupChild();
5071
5505
  process.exit(1);
5072
5506
  }
5073
5507
  serverSpinner.succeed(`Server running at ${c2.cyan(apiBase)}`);
@@ -5077,7 +5511,7 @@ async function cmdOpenClaw() {
5077
5511
  }
5078
5512
  }
5079
5513
  log2("");
5080
- log2(` ${c2.bold("Step 3 of 5")} ${c2.dim("\u2014")} ${c2.bold("Agent account")}`);
5514
+ log2(` ${c2.bold("Step 3 of 6")} ${c2.dim("\u2014")} ${c2.bold("Agent account")}`);
5081
5515
  log2("");
5082
5516
  let agentApiKey;
5083
5517
  let agentEmail = "";
@@ -5249,7 +5683,212 @@ async function cmdOpenClaw() {
5249
5683
  }
5250
5684
  }
5251
5685
  log2("");
5252
- log2(` ${c2.bold("Step 4 of 5")} ${c2.dim("\u2014")} ${c2.bold("Installing plugin + configuring OpenClaw")}`);
5686
+ log2(` ${c2.bold("Step 4 of 6")} ${c2.dim("\u2014")} ${c2.bold("Phone number access")} ${c2.green("NEW")}`);
5687
+ log2("");
5688
+ log2(` ${c2.dim("Give your AI agent a phone number via Google Voice.")}`);
5689
+ log2(` ${c2.dim("Agents can receive verification codes and send texts.")}`);
5690
+ log2("");
5691
+ let smsAlreadyConfigured = false;
5692
+ if (agentApiKey) {
5693
+ try {
5694
+ const smsResp = await fetch(`${apiBase}/api/agenticmail/sms/config`, {
5695
+ headers: { "Authorization": `Bearer ${agentApiKey}` },
5696
+ signal: AbortSignal.timeout(3e3)
5697
+ });
5698
+ const smsData = await smsResp.json();
5699
+ if (smsData.sms?.enabled) {
5700
+ smsAlreadyConfigured = true;
5701
+ ok2(`SMS already configured: ${c2.bold(smsData.sms.phoneNumber)}`);
5702
+ }
5703
+ } catch {
5704
+ }
5705
+ }
5706
+ if (!smsAlreadyConfigured) {
5707
+ const wantSms = await ask(` ${c2.bold("Set up phone number access?")} ${c2.dim("(y/N)")} `);
5708
+ if (wantSms.toLowerCase().startsWith("y")) {
5709
+ log2("");
5710
+ const hasVoice = await ask(` ${c2.bold("Do you already have a Google Voice number?")} ${c2.dim("(y/N)")} `);
5711
+ if (!hasVoice.toLowerCase().startsWith("y")) {
5712
+ log2("");
5713
+ log2(` ${c2.bold("Google Voice Setup (takes about 2 minutes):")}`);
5714
+ log2("");
5715
+ log2(` ${c2.cyan("Step 1:")} Open ${c2.bold(c2.cyan("https://voice.google.com"))} in your browser`);
5716
+ log2(` ${c2.cyan("Step 2:")} Sign in with your Google account`);
5717
+ log2(` ${c2.cyan("Step 3:")} Click ${c2.bold('"Choose a phone number"')}`);
5718
+ log2(` ${c2.cyan("Step 4:")} Search for a number by city or area code`);
5719
+ log2(` ${c2.cyan("Step 5:")} Pick a number and click ${c2.bold('"Verify"')}`);
5720
+ log2(` ${c2.dim("(Google will send a code to your existing phone to verify)")}`);
5721
+ log2(` ${c2.cyan("Step 6:")} Once verified, go to ${c2.bold("Settings")} (gear icon)`);
5722
+ log2(` ${c2.cyan("Step 7:")} Under Messages, enable ${c2.bold('"Forward messages to email"')}`);
5723
+ log2("");
5724
+ log2(` ${c2.dim("Come back here when you have your number.")}`);
5725
+ log2("");
5726
+ await ask(` ${c2.bold("Press Enter when ready...")} `);
5727
+ }
5728
+ log2("");
5729
+ const phoneNumber = await ask(` ${c2.bold("Your Google Voice number")} ${c2.dim("(e.g. +12125551234):")} `);
5730
+ if (phoneNumber.trim()) {
5731
+ const digits = phoneNumber.replace(/[^+\d]/g, "").replace(/\D/g, "");
5732
+ if (digits.length < 10) {
5733
+ log2(` ${c2.yellow("!")} That doesn't look like a valid phone number.`);
5734
+ info2("You can set this up later in the shell with /sms");
5735
+ } else {
5736
+ let forwardingEmail;
5737
+ let forwardingPassword;
5738
+ let relayEmail = "";
5739
+ let relayProvider = "";
5740
+ try {
5741
+ const gwResp = await fetch(`${apiBase}/api/agenticmail/gateway/status`, {
5742
+ headers: { "Authorization": `Bearer ${config.masterKey}` },
5743
+ signal: AbortSignal.timeout(5e3)
5744
+ });
5745
+ const gwData = await gwResp.json();
5746
+ if (gwData.mode === "relay" && gwData.relay?.email) {
5747
+ relayEmail = gwData.relay.email;
5748
+ relayProvider = gwData.relay.provider || "";
5749
+ } else if (gwData.mode === "domain") {
5750
+ relayProvider = "domain";
5751
+ }
5752
+ } catch {
5753
+ }
5754
+ log2("");
5755
+ log2(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510`);
5756
+ log2(` \u2502 ${c2.red(c2.bold("READ THIS"))} \u2014 Google Voice email matching is ${c2.red(c2.bold("critical"))} \u2502`);
5757
+ log2(` \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`);
5758
+ log2("");
5759
+ log2(` Google Voice forwards SMS ${c2.bold("ONLY")} to the ${c2.green(c2.bold("Gmail account"))}`);
5760
+ log2(` you used to ${c2.green(c2.bold("sign up"))} for Google Voice.`);
5761
+ log2("");
5762
+ log2(` ${c2.yellow("If your agent can't read that Gmail, it will")} ${c2.red(c2.bold("NEVER"))}`);
5763
+ log2(` ${c2.yellow("receive any SMS messages.")}`);
5764
+ log2("");
5765
+ if (relayEmail) {
5766
+ const isGmail = relayEmail.toLowerCase().endsWith("@gmail.com");
5767
+ log2(` Your email relay: ${c2.bold(c2.cyan(relayEmail))}`);
5768
+ if (!isGmail) {
5769
+ log2("");
5770
+ log2(` ${c2.red(c2.bold("!!"))} Your relay is ${c2.bold("not Gmail")}. Google Voice won't forward here.`);
5771
+ log2(` ${c2.red(c2.bold("!!"))} You ${c2.bold("must")} provide the Gmail you used for Google Voice.`);
5772
+ log2("");
5773
+ const gvEmail = await ask(` ${c2.green(c2.bold("Gmail used for Google Voice:"))} `);
5774
+ if (gvEmail.trim() && gvEmail.toLowerCase().includes("@gmail.com")) {
5775
+ log2("");
5776
+ log2(` ${c2.dim("Get an app password at:")} ${c2.cyan("https://myaccount.google.com/apppasswords")}`);
5777
+ const gvPass = await ask(` ${c2.green(c2.bold("App password for"))} ${c2.bold(gvEmail.trim())}: `);
5778
+ if (gvPass.trim()) {
5779
+ forwardingEmail = gvEmail.trim();
5780
+ forwardingPassword = gvPass.trim();
5781
+ } else {
5782
+ log2(` ${c2.red("!")} No password. ${c2.yellow("SMS will not work without this.")}`);
5783
+ log2(` ${c2.dim("Fix later with /sms in the shell.")}`);
5784
+ }
5785
+ } else if (gvEmail.trim()) {
5786
+ log2(` ${c2.red("!")} Google Voice requires a ${c2.bold("Gmail")} address (ends in @gmail.com).`);
5787
+ log2(` ${c2.dim("Fix later with /sms in the shell.")}`);
5788
+ } else {
5789
+ log2(` ${c2.yellow("!")} Skipped. ${c2.dim("SMS will not work until you provide this.")}`);
5790
+ log2(` ${c2.dim("Fix later with /sms in the shell.")}`);
5791
+ }
5792
+ } else {
5793
+ log2("");
5794
+ log2(` ${c2.yellow("?")} Did you sign up for Google Voice with ${c2.bold("this same Gmail")}?`);
5795
+ log2(` ${c2.dim("If you used a different Gmail for Google Voice, say no.")}`);
5796
+ log2("");
5797
+ const sameEmail = await ask(` ${c2.bold("Same Gmail as Google Voice?")} ${c2.dim("(Y/n)")} `);
5798
+ if (sameEmail.toLowerCase().startsWith("n")) {
5799
+ log2("");
5800
+ log2(` ${c2.yellow(c2.bold("Different Gmail detected."))} Your agent needs access to the`);
5801
+ log2(` Google Voice Gmail to receive SMS.`);
5802
+ log2("");
5803
+ const gvEmail = await ask(` ${c2.green(c2.bold("Gmail used for Google Voice:"))} `);
5804
+ if (gvEmail.trim() && gvEmail.toLowerCase().includes("@gmail.com")) {
5805
+ if (gvEmail.trim().toLowerCase() === relayEmail.toLowerCase()) {
5806
+ log2(` ${c2.green("!")} That's the same email as your relay \u2014 you're all set!`);
5807
+ } else {
5808
+ log2("");
5809
+ log2(` ${c2.dim("Get an app password at:")} ${c2.cyan("https://myaccount.google.com/apppasswords")}`);
5810
+ const gvPass = await ask(` ${c2.green(c2.bold("App password for"))} ${c2.bold(gvEmail.trim())}: `);
5811
+ if (gvPass.trim()) {
5812
+ forwardingEmail = gvEmail.trim();
5813
+ forwardingPassword = gvPass.trim();
5814
+ } else {
5815
+ log2(` ${c2.red("!")} No password. ${c2.yellow("SMS will not work without this.")}`);
5816
+ log2(` ${c2.dim("Fix later with /sms in the shell.")}`);
5817
+ }
5818
+ }
5819
+ } else if (gvEmail.trim()) {
5820
+ log2(` ${c2.red("!")} Google Voice requires a ${c2.bold("Gmail")} address.`);
5821
+ log2(` ${c2.dim("Fix later with /sms in the shell.")}`);
5822
+ } else {
5823
+ log2(` ${c2.yellow("!")} Skipped. ${c2.dim("SMS may not work if emails don't match.")}`);
5824
+ log2(` ${c2.dim("Fix later with /sms in the shell.")}`);
5825
+ }
5826
+ }
5827
+ }
5828
+ } else if (relayProvider === "domain") {
5829
+ log2(` ${c2.yellow("!")} You're using ${c2.bold("domain mode")} (no Gmail relay).`);
5830
+ log2(` ${c2.yellow("!")} Google Voice needs a Gmail. Provide the one you signed up with.`);
5831
+ log2("");
5832
+ const gvEmail = await ask(` ${c2.green(c2.bold("Gmail used for Google Voice:"))} `);
5833
+ if (gvEmail.trim() && gvEmail.toLowerCase().includes("@gmail.com")) {
5834
+ log2("");
5835
+ log2(` ${c2.dim("Get an app password at:")} ${c2.cyan("https://myaccount.google.com/apppasswords")}`);
5836
+ const gvPass = await ask(` ${c2.green(c2.bold("App password for"))} ${c2.bold(gvEmail.trim())}: `);
5837
+ if (gvPass.trim()) {
5838
+ forwardingEmail = gvEmail.trim();
5839
+ forwardingPassword = gvPass.trim();
5840
+ } else {
5841
+ log2(` ${c2.red("!")} No password. ${c2.yellow("SMS will not work without this.")}`);
5842
+ log2(` ${c2.dim("Fix later with /sms in the shell.")}`);
5843
+ }
5844
+ } else {
5845
+ log2(` ${c2.yellow("!")} Skipped. ${c2.dim("SMS will not work until you provide this.")}`);
5846
+ log2(` ${c2.dim("Fix later with /sms in the shell.")}`);
5847
+ }
5848
+ } else {
5849
+ log2(` ${c2.dim("No email relay detected yet.")}`);
5850
+ log2("");
5851
+ const gvEmail = await ask(` ${c2.green(c2.bold("Gmail used for Google Voice:"))} `);
5852
+ if (gvEmail.trim() && gvEmail.includes("@")) {
5853
+ forwardingEmail = gvEmail.trim();
5854
+ }
5855
+ }
5856
+ if (agentApiKey) {
5857
+ try {
5858
+ const body = { phoneNumber: phoneNumber.trim() };
5859
+ if (forwardingEmail) body.forwardingEmail = forwardingEmail;
5860
+ if (forwardingPassword) body.forwardingPassword = forwardingPassword;
5861
+ const resp = await fetch(`${apiBase}/api/agenticmail/sms/setup`, {
5862
+ method: "POST",
5863
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${agentApiKey}` },
5864
+ body: JSON.stringify(body)
5865
+ });
5866
+ const data = await resp.json();
5867
+ if (data.success) {
5868
+ log2("");
5869
+ ok2(`Phone number saved: ${c2.bold(data.sms?.phoneNumber || phoneNumber.trim())}`);
5870
+ if (forwardingEmail) {
5871
+ ok2(`SMS forwarding via: ${c2.bold(forwardingEmail)}`);
5872
+ }
5873
+ log2(` ${c2.dim('Remember: enable "Forward messages to email" in Google Voice settings')}`);
5874
+ log2(` ${c2.dim("Manage SMS anytime in the shell with /sms")}`);
5875
+ } else {
5876
+ fail2(data.error || "Setup failed");
5877
+ }
5878
+ } catch (err) {
5879
+ fail2(err.message);
5880
+ }
5881
+ }
5882
+ }
5883
+ } else {
5884
+ info2("Skipped. Use /sms in the shell anytime.");
5885
+ }
5886
+ } else {
5887
+ info2("Skipped. Add a phone number anytime with /sms in the shell.");
5888
+ }
5889
+ }
5890
+ log2("");
5891
+ log2(` ${c2.bold("Step 5 of 6")} ${c2.dim("\u2014")} ${c2.bold("Installing plugin + configuring OpenClaw")}`);
5253
5892
  log2("");
5254
5893
  const pluginDir = resolveOpenClawPluginDir();
5255
5894
  if (pluginDir) {
@@ -5323,7 +5962,7 @@ async function cmdOpenClaw() {
5323
5962
  }
5324
5963
  }
5325
5964
  log2("");
5326
- log2(` ${c2.bold("Step 5 of 5")} ${c2.dim("\u2014")} ${c2.bold("Restarting OpenClaw gateway")}`);
5965
+ log2(` ${c2.bold("Step 6 of 6")} ${c2.dim("\u2014")} ${c2.bold("Restarting OpenClaw gateway")}`);
5327
5966
  log2("");
5328
5967
  let gatewayRestarted = false;
5329
5968
  let hasOpenClawCli = false;
@@ -5371,27 +6010,69 @@ async function cmdOpenClaw() {
5371
6010
  }
5372
6011
  log2(` ${c2.dim("Master Key:")} ${c2.yellow(config.masterKey)}`);
5373
6012
  log2(` ${c2.dim("Server:")} ${c2.cyan(apiBase)}`);
6013
+ let smsPhone = "";
6014
+ if (agentApiKey) {
6015
+ try {
6016
+ const smsResp = await fetch(`${apiBase}/api/agenticmail/sms/config`, {
6017
+ headers: { "Authorization": `Bearer ${agentApiKey}` },
6018
+ signal: AbortSignal.timeout(3e3)
6019
+ });
6020
+ const smsData = await smsResp.json();
6021
+ if (smsData.sms?.enabled && smsData.sms?.phoneNumber) {
6022
+ smsPhone = smsData.sms.phoneNumber;
6023
+ log2(` ${c2.dim("Phone:")} ${c2.green(smsPhone)} ${c2.dim("via Google Voice")}`);
6024
+ }
6025
+ } catch {
6026
+ }
6027
+ }
6028
+ if (!smsPhone) {
6029
+ try {
6030
+ const acctResp = await fetch(`${apiBase}/api/agenticmail/accounts`, {
6031
+ headers: { "Authorization": `Bearer ${config.masterKey}` },
6032
+ signal: AbortSignal.timeout(3e3)
6033
+ });
6034
+ const acctData = await acctResp.json();
6035
+ for (const a of acctData.agents || []) {
6036
+ const smsConf = a.metadata?.sms;
6037
+ if (smsConf?.enabled && smsConf.phoneNumber) {
6038
+ smsPhone = smsConf.phoneNumber;
6039
+ log2(` ${c2.dim("Phone:")} ${c2.green(smsPhone)} ${c2.dim("via Google Voice")} ${c2.dim("(" + a.name + ")")}`);
6040
+ break;
6041
+ }
6042
+ }
6043
+ } catch {
6044
+ }
6045
+ }
5374
6046
  log2("");
5375
6047
  if (gatewayRestarted) {
5376
- log2(` Your agent now has ${c2.bold("54 email tools")} available!`);
6048
+ log2(` Your agent now has ${c2.bold("63 email + SMS tools")} available!`);
5377
6049
  log2(` Try: ${c2.dim('"Send an email to test@example.com"')}`);
5378
6050
  log2("");
5379
6051
  log2(` ${c2.bold("\u{1F380} AgenticMail Coordination")} ${c2.dim("(auto-configured)")}`);
5380
6052
  log2(` Your agent can now use ${c2.cyan("agenticmail_call_agent")} to call other agents`);
5381
6053
  log2(` with structured task queues, push notifications, and auto-spawned sessions.`);
5382
6054
  log2(` This replaces sessions_spawn for coordinated multi-agent work.`);
6055
+ log2("");
6056
+ if (smsPhone) {
6057
+ log2(` ${c2.bold("\u{1F4F1} SMS & Phone Access")} ${c2.green("ACTIVE")}`);
6058
+ log2(` Phone: ${c2.bold(smsPhone)} via Google Voice`);
6059
+ log2(` Your agent can receive verification codes and send texts.`);
6060
+ log2(` Manage with ${c2.cyan("/sms")} in the shell.`);
6061
+ } else {
6062
+ log2(` ${c2.bold("\u{1F4F1} SMS & Phone Access")} ${c2.dim("(Google Voice)")}`);
6063
+ log2(` Your agent can receive verification codes and send texts.`);
6064
+ log2(` SMS messages are auto-detected from Google Voice email forwarding.`);
6065
+ log2(` Set up with ${c2.cyan("/sms")} in the shell or during setup wizard.`);
6066
+ }
5383
6067
  } else {
5384
6068
  log2(` ${c2.bold("Next step:")}`);
5385
6069
  log2(` Restart your OpenClaw gateway, then your agent will`);
5386
- log2(` have ${c2.bold("54 email tools")} available!`);
6070
+ log2(` have ${c2.bold("63 email + SMS tools")} available!`);
5387
6071
  }
5388
6072
  log2("");
5389
6073
  if (process.stdin.isTTY) {
5390
- await interactiveShell({ config, onExit: cleanupChild });
5391
- } else {
5392
- if (!serverWasRunning) {
5393
- cleanupChild();
5394
- }
6074
+ await interactiveShell({ config, onExit: () => {
6075
+ } });
5395
6076
  }
5396
6077
  }
5397
6078
  function printPluginSnippet(apiUrl, masterKey, agentApiKey) {
@@ -5532,61 +6213,118 @@ async function cmdStart() {
5532
6213
  }
5533
6214
  const serverSpinner = new Spinner("server", "Launching your server...");
5534
6215
  serverSpinner.start();
5535
- let alreadyRunning = false;
5536
6216
  try {
5537
- const probe = await fetch(`http://${config.api.host}:${config.api.port}/api/agenticmail/health`, {
5538
- signal: AbortSignal.timeout(2e3)
5539
- });
5540
- if (probe.ok) alreadyRunning = true;
5541
- } catch {
6217
+ const ready = await startApiServer(config);
6218
+ if (ready) {
6219
+ serverSpinner.succeed(`Server running at ${c2.cyan(`http://${config.api.host}:${config.api.port}`)}`);
6220
+ } else {
6221
+ serverSpinner.fail("Server did not start in time");
6222
+ process.exit(1);
6223
+ }
6224
+ } catch (err) {
6225
+ serverSpinner.fail(`Couldn't start the server: ${err.message}`);
6226
+ process.exit(1);
5542
6227
  }
5543
- if (alreadyRunning) {
5544
- serverSpinner.succeed(`Server already running at ${c2.cyan(`http://${config.api.host}:${config.api.port}`)}`);
6228
+ await interactiveShell({ config, onExit: () => {
6229
+ } });
6230
+ }
6231
+ async function cmdStop() {
6232
+ log2("");
6233
+ const stopped = stopApiServer();
6234
+ if (stopped) {
6235
+ ok2("AgenticMail server stopped");
5545
6236
  } else {
6237
+ info2("Server is not running");
6238
+ }
6239
+ log2("");
6240
+ }
6241
+ async function cmdUpdate() {
6242
+ const { execSync } = await import("child_process");
6243
+ log2("");
6244
+ log2(` ${c2.dim("\u2500".repeat(50))}`);
6245
+ log2(` ${c2.bold("Update AgenticMail")}`);
6246
+ log2("");
6247
+ let currentVersion = "unknown";
6248
+ try {
6249
+ const { readFileSync: readFileSync3 } = await import("fs");
6250
+ const { join: join2, dirname: dirname2 } = await import("path");
6251
+ const { fileURLToPath: fileURLToPath2 } = await import("url");
6252
+ const thisDir = dirname2(fileURLToPath2(import.meta.url));
6253
+ const pkg = JSON.parse(readFileSync3(join2(thisDir, "..", "package.json"), "utf-8"));
6254
+ currentVersion = pkg.version ?? "unknown";
6255
+ } catch {
6256
+ }
6257
+ info2(`Current version: ${c2.bold(currentVersion)}`);
6258
+ let latestVersion = "unknown";
6259
+ try {
6260
+ latestVersion = execSync("npm view agenticmail version", { encoding: "utf-8", timeout: 15e3 }).trim();
6261
+ } catch {
6262
+ fail2("Could not check npm. Check your internet connection.");
6263
+ process.exit(1);
6264
+ }
6265
+ info2(`Latest version: ${c2.bold(latestVersion)}`);
6266
+ if (currentVersion === latestVersion) {
6267
+ ok2("Already on the latest version!");
6268
+ log2("");
6269
+ process.exit(0);
6270
+ }
6271
+ info2(`New version available: ${c2.yellow(currentVersion)} \u2192 ${c2.green(latestVersion)}`);
6272
+ log2("");
6273
+ let hasOpenClaw = false;
6274
+ try {
6275
+ execSync("which openclaw", { stdio: "ignore", timeout: 5e3 });
6276
+ hasOpenClaw = true;
6277
+ const ocVersion = execSync('openclaw --version 2>/dev/null || echo "?"', { encoding: "utf-8", timeout: 1e4 }).trim();
6278
+ info2(`OpenClaw detected: ${c2.bold(ocVersion)}`);
6279
+ } catch {
6280
+ }
6281
+ let pm = "npm";
6282
+ try {
6283
+ execSync("pnpm --version", { stdio: "ignore", timeout: 5e3 });
6284
+ pm = "pnpm";
6285
+ } catch {
5546
6286
  try {
5547
- const { fork } = await import("child_process");
5548
- const apiEntry = resolveApiEntry();
5549
- if (!existsSync2(apiEntry)) {
5550
- serverSpinner.fail(`Server isn't built yet. Run: ${c2.bold("npm run build")}`);
5551
- process.exit(1);
5552
- }
5553
- const env = configToEnv(config);
5554
- apiChild = fork(apiEntry, [], { stdio: ["ignore", "ignore", "pipe", "ipc"], env });
5555
- const stderrLines = [];
5556
- apiChild.stderr?.on("data", (chunk) => {
5557
- const lines = chunk.toString().trim().split("\n");
5558
- for (const line of lines) {
5559
- stderrLines.push(line);
5560
- if (stderrLines.length > 50) stderrLines.shift();
5561
- }
5562
- });
5563
- apiChild.on("exit", (code, signal) => {
5564
- apiChild = null;
5565
- log2("");
5566
- fail2(`Server stopped unexpectedly${signal ? ` (signal: ${signal})` : code ? ` (exit code: ${code})` : ""}`);
5567
- if (stderrLines.length > 0) {
5568
- log2("");
5569
- log2(` ${c2.dim("Last server output:")}`);
5570
- for (const line of stderrLines.slice(-10)) {
5571
- log2(` ${c2.dim(line)}`);
5572
- }
5573
- }
5574
- log2("");
5575
- process.exit(code ?? 1);
5576
- });
5577
- const ready = await waitForApi(config.api.host, config.api.port, 2e4);
5578
- if (!ready) {
5579
- serverSpinner.fail("Server did not start in time");
5580
- cleanupChild();
5581
- process.exit(1);
6287
+ execSync("bun --version", { stdio: "ignore", timeout: 5e3 });
6288
+ pm = "bun";
6289
+ } catch {
6290
+ }
6291
+ }
6292
+ let isGlobal = false;
6293
+ try {
6294
+ const list = execSync(`npm list -g agenticmail 2>/dev/null`, { encoding: "utf-8", timeout: 1e4 });
6295
+ if (list.includes("agenticmail@")) isGlobal = true;
6296
+ } catch {
6297
+ }
6298
+ const scope = isGlobal ? "-g" : "";
6299
+ const installCmd = pm === "bun" ? `bun add ${scope} agenticmail@latest`.trim() : `${pm} install ${scope} agenticmail@latest`.trim();
6300
+ info2(`Running: ${c2.dim(installCmd)}`);
6301
+ try {
6302
+ execSync(installCmd, { stdio: "inherit", timeout: 12e4 });
6303
+ ok2(`Updated to agenticmail@${latestVersion}`);
6304
+ } catch (err) {
6305
+ fail2(`Update failed: ${err.message}`);
6306
+ info2(`Try: ${c2.green("npm install -g agenticmail@latest")}`);
6307
+ process.exit(1);
6308
+ }
6309
+ if (hasOpenClaw) {
6310
+ const pluginCmd = pm === "bun" ? `bun add ${scope} @agenticmail/openclaw@latest`.trim() : `${pm} install ${scope} @agenticmail/openclaw@latest`.trim();
6311
+ info2(`Updating OpenClaw plugin: ${c2.dim(pluginCmd)}`);
6312
+ try {
6313
+ execSync(pluginCmd, { stdio: "inherit", timeout: 12e4 });
6314
+ ok2("OpenClaw plugin updated.");
6315
+ try {
6316
+ execSync("openclaw gateway restart", { stdio: "pipe", timeout: 3e4 });
6317
+ ok2("OpenClaw gateway restarted.");
6318
+ } catch {
6319
+ info2(`Restart OpenClaw: ${c2.green("openclaw gateway restart")}`);
5582
6320
  }
5583
- serverSpinner.succeed(`Server running at ${c2.cyan(`http://${config.api.host}:${config.api.port}`)}`);
5584
- } catch (err) {
5585
- serverSpinner.fail(`Couldn't start the server: ${err.message}`);
5586
- process.exit(1);
6321
+ } catch {
6322
+ info2(`Update plugin manually: ${c2.green(pluginCmd)}`);
5587
6323
  }
5588
6324
  }
5589
- await interactiveShell({ config, onExit: cleanupChild });
6325
+ log2("");
6326
+ ok2("Update complete!");
6327
+ log2("");
5590
6328
  }
5591
6329
  var command = process.argv[2];
5592
6330
  switch (command) {
@@ -5602,6 +6340,14 @@ switch (command) {
5602
6340
  process.exit(1);
5603
6341
  });
5604
6342
  break;
6343
+ case "stop":
6344
+ cmdStop().then(() => {
6345
+ process.exit(0);
6346
+ }).catch((err) => {
6347
+ console.error(err);
6348
+ process.exit(1);
6349
+ });
6350
+ break;
5605
6351
  case "status":
5606
6352
  cmdStatus().then(() => {
5607
6353
  process.exit(0);
@@ -5616,6 +6362,12 @@ switch (command) {
5616
6362
  process.exit(1);
5617
6363
  });
5618
6364
  break;
6365
+ case "update":
6366
+ cmdUpdate().catch((err) => {
6367
+ console.error(err);
6368
+ process.exit(1);
6369
+ });
6370
+ break;
5619
6371
  case "help":
5620
6372
  case "--help":
5621
6373
  case "-h":
@@ -5626,8 +6378,10 @@ switch (command) {
5626
6378
  log2(` ${c2.green("agenticmail")} Get started (setup + start)`);
5627
6379
  log2(` ${c2.green("agenticmail setup")} Re-run the setup wizard`);
5628
6380
  log2(` ${c2.green("agenticmail start")} Start the server`);
6381
+ log2(` ${c2.green("agenticmail stop")} Stop the server`);
5629
6382
  log2(` ${c2.green("agenticmail status")} See what's running`);
5630
6383
  log2(` ${c2.green("agenticmail openclaw")} Set up AgenticMail for OpenClaw`);
6384
+ log2(` ${c2.green("agenticmail update")} Update to the latest version`);
5631
6385
  log2("");
5632
6386
  process.exit(0);
5633
6387
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenticmail",
3
- "version": "0.3.24",
3
+ "version": "0.4.0",
4
4
  "description": "Email infrastructure for AI agents — send and receive real email programmatically",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",