agenticmail 0.3.25 → 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 +806 -12
  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
@@ -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 () => {
@@ -4109,7 +4449,7 @@ async function cmdSetup() {
4109
4449
  log2(` needs to send and receive real email.`);
4110
4450
  log2("");
4111
4451
  const hasOpenClaw = existsSync2(join(homedir(), ".openclaw", "openclaw.json"));
4112
- const totalSteps = hasOpenClaw ? 5 : 4;
4452
+ const totalSteps = hasOpenClaw ? 6 : 5;
4113
4453
  log2(` Here's what we'll do:`);
4114
4454
  log2(` ${c2.dim("1.")} Check your system for required tools`);
4115
4455
  log2(` ${c2.dim("2.")} Create your private account and keys`);
@@ -4377,9 +4717,117 @@ async function cmdSetup() {
4377
4717
  } else if (!existingEmail) {
4378
4718
  info2("No problem! You can set up email anytime by running this again.");
4379
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
+ }
4380
4828
  if (hasOpenClaw && serverReady) {
4381
4829
  log2("");
4382
- 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")}`);
4383
4831
  log2("");
4384
4832
  await registerWithOpenClaw(result.config);
4385
4833
  }
@@ -4968,11 +5416,12 @@ async function cmdOpenClaw() {
4968
5416
  log2(` This will:`);
4969
5417
  log2(` ${c2.dim("1.")} Set up the mail server infrastructure`);
4970
5418
  log2(` ${c2.dim("2.")} Create an agent email account`);
4971
- log2(` ${c2.dim("3.")} Configure the OpenClaw plugin`);
4972
- 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`);
4973
5422
  log2("");
4974
5423
  const setup = new SetupManager();
4975
- 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")}`);
4976
5425
  log2("");
4977
5426
  let config;
4978
5427
  let configPath;
@@ -5031,7 +5480,7 @@ async function cmdOpenClaw() {
5031
5480
  }
5032
5481
  }
5033
5482
  log2("");
5034
- 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")}`);
5035
5484
  log2("");
5036
5485
  const apiHost = config.api.host;
5037
5486
  const apiPort = config.api.port;
@@ -5062,7 +5511,7 @@ async function cmdOpenClaw() {
5062
5511
  }
5063
5512
  }
5064
5513
  log2("");
5065
- 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")}`);
5066
5515
  log2("");
5067
5516
  let agentApiKey;
5068
5517
  let agentEmail = "";
@@ -5234,7 +5683,212 @@ async function cmdOpenClaw() {
5234
5683
  }
5235
5684
  }
5236
5685
  log2("");
5237
- 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")}`);
5238
5892
  log2("");
5239
5893
  const pluginDir = resolveOpenClawPluginDir();
5240
5894
  if (pluginDir) {
@@ -5308,7 +5962,7 @@ async function cmdOpenClaw() {
5308
5962
  }
5309
5963
  }
5310
5964
  log2("");
5311
- 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")}`);
5312
5966
  log2("");
5313
5967
  let gatewayRestarted = false;
5314
5968
  let hasOpenClawCli = false;
@@ -5356,19 +6010,64 @@ async function cmdOpenClaw() {
5356
6010
  }
5357
6011
  log2(` ${c2.dim("Master Key:")} ${c2.yellow(config.masterKey)}`);
5358
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
+ }
5359
6046
  log2("");
5360
6047
  if (gatewayRestarted) {
5361
- log2(` Your agent now has ${c2.bold("54 email tools")} available!`);
6048
+ log2(` Your agent now has ${c2.bold("63 email + SMS tools")} available!`);
5362
6049
  log2(` Try: ${c2.dim('"Send an email to test@example.com"')}`);
5363
6050
  log2("");
5364
6051
  log2(` ${c2.bold("\u{1F380} AgenticMail Coordination")} ${c2.dim("(auto-configured)")}`);
5365
6052
  log2(` Your agent can now use ${c2.cyan("agenticmail_call_agent")} to call other agents`);
5366
6053
  log2(` with structured task queues, push notifications, and auto-spawned sessions.`);
5367
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
+ }
5368
6067
  } else {
5369
6068
  log2(` ${c2.bold("Next step:")}`);
5370
6069
  log2(` Restart your OpenClaw gateway, then your agent will`);
5371
- log2(` have ${c2.bold("54 email tools")} available!`);
6070
+ log2(` have ${c2.bold("63 email + SMS tools")} available!`);
5372
6071
  }
5373
6072
  log2("");
5374
6073
  if (process.stdin.isTTY) {
@@ -5539,6 +6238,94 @@ async function cmdStop() {
5539
6238
  }
5540
6239
  log2("");
5541
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 {
6286
+ try {
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")}`);
6320
+ }
6321
+ } catch {
6322
+ info2(`Update plugin manually: ${c2.green(pluginCmd)}`);
6323
+ }
6324
+ }
6325
+ log2("");
6326
+ ok2("Update complete!");
6327
+ log2("");
6328
+ }
5542
6329
  var command = process.argv[2];
5543
6330
  switch (command) {
5544
6331
  case "setup":
@@ -5575,6 +6362,12 @@ switch (command) {
5575
6362
  process.exit(1);
5576
6363
  });
5577
6364
  break;
6365
+ case "update":
6366
+ cmdUpdate().catch((err) => {
6367
+ console.error(err);
6368
+ process.exit(1);
6369
+ });
6370
+ break;
5578
6371
  case "help":
5579
6372
  case "--help":
5580
6373
  case "-h":
@@ -5588,6 +6381,7 @@ switch (command) {
5588
6381
  log2(` ${c2.green("agenticmail stop")} Stop the server`);
5589
6382
  log2(` ${c2.green("agenticmail status")} See what's running`);
5590
6383
  log2(` ${c2.green("agenticmail openclaw")} Set up AgenticMail for OpenClaw`);
6384
+ log2(` ${c2.green("agenticmail update")} Update to the latest version`);
5591
6385
  log2("");
5592
6386
  process.exit(0);
5593
6387
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenticmail",
3
- "version": "0.3.25",
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",