@floomhq/signaldash 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # SignalDash
2
2
 
3
- Secure LinkedIn and WhatsApp access for AI agents.
3
+ Secure LinkedIn, WhatsApp, and email access for AI agents.
4
4
 
5
5
  `@floomhq/signaldash` connects your accounts through the hosted SignalDash
6
- service and exposes six account-scoped MCP tools. The CLI never receives or
7
- stores the server's Unipile access key.
6
+ service and exposes account-scoped MCP tools. The CLI never receives or stores
7
+ the server's Unipile access key.
8
8
 
9
9
  ## Quickstart
10
10
 
@@ -15,12 +15,14 @@ SignalDash administrator for an invite code, then run:
15
15
  npx -y @floomhq/signaldash login <invite-code>
16
16
  npx -y @floomhq/signaldash connect linkedin
17
17
  npx -y @floomhq/signaldash connect whatsapp
18
+ npx -y @floomhq/signaldash connect email
18
19
  ```
19
20
 
20
21
  Each connect command prints a short-lived hosted-auth URL and waits for the
21
22
  connection to finish. Open the LinkedIn URL and sign in, or open the WhatsApp
22
- URL and scan the live QR code. The CLI prints `Connected linkedin` or
23
- `Connected whatsapp` when the account is ready.
23
+ URL and scan the live QR code. The email link offers Google, Outlook, and IMAP
24
+ when those providers are enabled on the Unipile account. The CLI prints
25
+ `Connected <provider>` when the account is ready.
24
26
 
25
27
  The invite code is single-use. Login stores a SignalDash user token in
26
28
  `~/.signaldash/config.json`; the Unipile access key remains on the SignalDash
@@ -69,8 +71,8 @@ npx -y @floomhq/signaldash mcp
69
71
  ```
70
72
 
71
73
  The MCP server uses the user token created by `login`. It cannot access a
72
- LinkedIn or WhatsApp account until that channel has been connected for the
73
- same logged-in user.
74
+ LinkedIn, WhatsApp, or email account until that channel has been connected for
75
+ the same logged-in user.
74
76
 
75
77
  ## Give the agent the safety skill
76
78
 
@@ -95,6 +97,9 @@ SignalDash exposes:
95
97
  - `wa_list_chats`
96
98
  - `wa_read_messages(chat_id)`
97
99
  - `wa_send_message(chat_id, text)`
100
+ - `email_list(limit)`
101
+ - `email_read(thread_id, limit)`
102
+ - `email_send(to, subject, body)`
98
103
 
99
104
  Every operation runs through the hosted SignalDash backend. Agents never
100
105
  receive the Unipile access key.
@@ -109,6 +114,8 @@ workflow around that runtime control:
109
114
  exhausted, SignalDash returns HTTP 429 with `code: "rate_limit_exceeded"` and
110
115
  `Retry-After`. Usage is persisted server-side and resets at midnight UTC.
111
116
  - Read the exact thread before every send.
117
+ - Email sends accept one recipient at a time and require a recent
118
+ `email_read` containing that address.
112
119
  - Never infer a recipient from a partial name.
113
120
  - Never send a duplicate or retry an ambiguous timeout without re-reading.
114
121
  - Do not parallelize sends or work around a rate limit.
package/bin/sd.mjs CHANGED
@@ -105,11 +105,18 @@ export async function cmdConnect(provider, dependencies = {}) {
105
105
  const now = dependencies.now || Date.now;
106
106
  const log = dependencies.log || console.log;
107
107
  const error = dependencies.error || console.error;
108
- if (!["linkedin", "whatsapp"].includes(provider)) {
109
- error("usage: signaldash connect linkedin|whatsapp");
108
+ if (!["linkedin", "whatsapp", "email"].includes(provider)) {
109
+ error("usage: signaldash connect linkedin|whatsapp|email");
110
110
  process.exitCode = 1;
111
111
  return;
112
112
  }
113
+ // Check first: if the human already authenticated (or is already connected),
114
+ // confirm that instead of minting a confusing second link.
115
+ const pre = await request(`/connect/${provider}/status`, undefined, { method: "GET" });
116
+ if (pre.status === 200 && pre.json.connected) {
117
+ log(` Connected ${provider}${pre.json.name ? `: ${pre.json.name}` : ""}`);
118
+ return;
119
+ }
113
120
  const r = await request(`/connect/${provider}`, {});
114
121
  if (r.status >= 300) {
115
122
  error(r.json.error === "login required"
@@ -118,10 +125,18 @@ export async function cmdConnect(provider, dependencies = {}) {
118
125
  process.exitCode = 1;
119
126
  return;
120
127
  }
121
- const { chalk, ora, open } = await ui();
128
+ const { chalk, ora, open: defaultOpen } = await ui();
129
+ const openUrl = dependencies.open || defaultOpen;
122
130
  log("\n " + chalk.dim("Opening your browser. If it does not open, use this link:"));
123
131
  log(" " + chalk.cyan(r.json.url) + "\n");
124
- try { await open(r.json.url); } catch {}
132
+ try { await openUrl(r.json.url); } catch {}
133
+ if (!(process.stdin.isTTY && process.stdout.isTTY) && !dependencies.forcePoll) {
134
+ log("");
135
+ log(` ACTION REQUIRED (human): open the link above to connect ${provider}.`);
136
+ log(` Then run: npx @floomhq/signaldash connect ${provider}`);
137
+ log("");
138
+ return;
139
+ }
125
140
  const spin = ora({ text: `Waiting for ${provider} authentication...`, indent: 2 }).start();
126
141
  dependencies._spin = spin;
127
142
 
@@ -164,14 +179,56 @@ const TOOLS = [
164
179
  { name: "wa_list_chats", ch: "wa", action: "list_chats", description: "List your WhatsApp chats." },
165
180
  { name: "wa_read_messages", ch: "wa", action: "read", description: "Read messages in a WhatsApp chat. args: chat_id" },
166
181
  { name: "wa_send_message", ch: "wa", action: "send", description: "Send a WhatsApp message (rate-safe). args: chat_id, text" },
182
+ {
183
+ name: "email_list",
184
+ path: "/email/list",
185
+ description: "List the newest message from each recent email thread. args: limit",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: { limit: { type: "integer", minimum: 1, maximum: 100 } },
189
+ },
190
+ },
191
+ {
192
+ name: "email_read",
193
+ path: "/email/read",
194
+ description: "Read an email thread before drafting or sending. args: thread_id, limit",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ thread_id: { type: "string", minLength: 1, maxLength: 500 },
199
+ limit: { type: "integer", minimum: 1, maximum: 100 },
200
+ },
201
+ required: ["thread_id"],
202
+ },
203
+ },
204
+ {
205
+ name: "email_send",
206
+ path: "/email/send",
207
+ description: "Send one approved email after reading that recipient's thread. args: to, subject, body",
208
+ inputSchema: {
209
+ type: "object",
210
+ properties: {
211
+ to: {
212
+ type: "array",
213
+ items: { type: "string", format: "email" },
214
+ minItems: 1,
215
+ maxItems: 1,
216
+ },
217
+ subject: { type: "string", minLength: 1, maxLength: 998 },
218
+ body: { type: "string", minLength: 1, maxLength: 5000 },
219
+ },
220
+ required: ["to", "subject", "body"],
221
+ },
222
+ },
167
223
  { name: "li_my_posts", path: "/li/posts", description: "List the user's own LinkedIn posts with engagement counts (reactions, comments, impressions). args: limit" },
168
224
  { name: "li_post_reactions", path: "/li/post_reactions", description: "Who reacted to a post — name + headline. These are warm inbound signals. args: post_id, limit" },
169
225
  { name: "li_post_comments", path: "/li/post_comments", description: "Comments on a post, with author. args: post_id, limit" },
170
226
  { name: "li_draft_post", path: "/li/create_post", description: "Draft a LinkedIn post. Returns the draft WITHOUT publishing. Publishing requires the human to approve and re-send with publish:true. args: text" },
171
227
  ];
172
228
  function mcpTool(name) {
173
- return { name, description: TOOLS.find(t => t.name === name).description,
174
- inputSchema: { type: "object", properties: { chat_id: { type: "string" }, text: { type: "string" }, limit: { type: "number" }, post_id: { type: "string" }, member_id: { type: "string" }, publish: { type: "boolean" } } } };
229
+ const tool = TOOLS.find(t => t.name === name);
230
+ return { name, description: tool.description,
231
+ inputSchema: tool.inputSchema || { type: "object", properties: { chat_id: { type: "string" }, text: { type: "string" }, limit: { type: "number" }, post_id: { type: "string" }, member_id: { type: "string" }, publish: { type: "boolean" } } } };
175
232
  }
176
233
  export async function runMcp(dependencies = {}) {
177
234
  const input = dependencies.input || process.stdin;
@@ -235,7 +292,14 @@ export async function cmdSetup(code, dependencies = {}) {
235
292
  }
236
293
  log("");
237
294
 
238
- const choice = dependencies.choice ?? (await prompts({
295
+ // An AGENT (Claude Code / Cursor) runs this with no TTY. Blocking on an
296
+ // interactive menu is why "nothing happened" for the first testers: the
297
+ // process stopped at a prompt nobody could answer. Non-interactive => connect
298
+ // both and print links the agent can relay to the human.
299
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
300
+ const choice = dependencies.choice ?? (!interactive
301
+ ? ["linkedin", "whatsapp"]
302
+ : (await prompts({
239
303
  type: "select",
240
304
  name: "value",
241
305
  message: "Which accounts do you want to connect?",
@@ -246,7 +310,7 @@ export async function cmdSetup(code, dependencies = {}) {
246
310
  { title: "Skip for now", value: [] },
247
311
  ],
248
312
  initial: 0,
249
- })).value;
313
+ })).value);
250
314
 
251
315
  if (!choice || choice.length === 0) {
252
316
  log("\n " + chalk.dim("Nothing connected. Run `signaldash connect whatsapp` or `connect linkedin` anytime."));
@@ -269,7 +333,7 @@ export async function cmdStatus(dependencies = {}) {
269
333
  const request = dependencies.request || api;
270
334
  log("");
271
335
  log(" " + chalk.bold("SignalDash") + chalk.dim(" " + (cfg.backend || DEFAULT_BACKEND)));
272
- for (const provider of ["linkedin", "whatsapp"]) {
336
+ for (const provider of ["linkedin", "whatsapp", "email"]) {
273
337
  const r = await request(`/connect/${provider}/status`, undefined, { method: "GET" });
274
338
  const ok = r.status === 200 && r.json.connected;
275
339
  log(" " + (ok ? chalk.green("+") : chalk.dim("-")) + " " + provider.padEnd(9) +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/signaldash",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Secure LinkedIn and WhatsApp MCP access for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,27 +1,35 @@
1
1
  ---
2
2
  name: signaldash
3
- description: Give this agent secure access to the user's LinkedIn and WhatsApp — list chats, read threads, and send messages through SignalDash. Use whenever the user asks to check, read, triage, reply to, or send LinkedIn/WhatsApp messages. Also covers first-time setup.
3
+ description: Give this agent secure access to the user's LinkedIn, WhatsApp, and email accounts through SignalDash. Use whenever the user asks to check, read, triage, draft, reply to, or send LinkedIn/WhatsApp messages or email. Also covers first-time setup.
4
4
  ---
5
5
 
6
- # SignalDash LinkedIn + WhatsApp for this agent
6
+ # SignalDash: LinkedIn, WhatsApp, and email for this agent
7
7
 
8
8
  SignalDash is the gate between you and the user's real messaging accounts. The
9
9
  user's channel credentials live on the SignalDash server, never on their machine
10
10
  and never in your context. You act through SignalDash tools only.
11
11
 
12
- ## Setup (only if the tools are not available yet)
12
+ ## Setup (run this when the SignalDash tools are not available yet)
13
13
 
14
- The user needs an invite code from Federico, then:
14
+ You are an agent, so there is no interactive terminal. Run the command, then
15
+ RELAY THE LINKS TO THE HUMAN. They must click them; you cannot.
15
16
 
16
17
  ```bash
17
- npx @floomhq/signaldash login <invite-code>
18
- npx @floomhq/signaldash connect linkedin # opens a link; user authenticates; auto-detects
19
- npx @floomhq/signaldash connect whatsapp # same, QR scan from WhatsApp > Linked Devices
20
- claude mcp add signaldash -- npx -y @floomhq/signaldash mcp
18
+ npx -y @floomhq/signaldash <invite-code>
21
19
  ```
22
20
 
23
- Cursor: add to `.cursor/mcp.json`
24
- `{"mcpServers":{"signaldash":{"command":"npx","args":["-y","@floomhq/signaldash","mcp"]}}}`
21
+ This logs in, installs this skill, registers the MCP server, and prints one
22
+ `account.unipile.com` link per channel. Then:
23
+
24
+ 1. **Show the human the links** and tell them plainly: open this to connect
25
+ LinkedIn / WhatsApp. WhatsApp is a QR scan from WhatsApp > Linked Devices.
26
+ 2. After they say they have authenticated, confirm with:
27
+ `npx -y @floomhq/signaldash connect linkedin` (or `whatsapp`, or `email`)
28
+ 3. Check what is connected any time: `npx -y @floomhq/signaldash status`
29
+ 4. Restart the MCP connection (or the editor) so the tools load.
30
+
31
+ If the human has no invite code, they need one from Federico. Without it,
32
+ nothing else in this skill works.
25
33
 
26
34
  ## Tools
27
35
 
@@ -33,13 +41,19 @@ Cursor: add to `.cursor/mcp.json` →
33
41
  | `wa_list_chats` | list the user's WhatsApp chats |
34
42
  | `wa_read_messages` | read a WhatsApp thread (`chat_id`) |
35
43
  | `wa_send_message` | send a WhatsApp message (`chat_id`, `text`) |
44
+ | `email_list` | list the newest message from each recent email thread (`limit`) |
45
+ | `email_read` | read an email thread (`thread_id`, `limit`) |
46
+ | `email_send` | send one email (`to`, `subject`, `body`) |
36
47
 
37
48
  ## Enforced by the server (you cannot bypass these)
38
49
 
39
50
  - **Read before send.** Sending to a chat you have not read recently returns
40
51
  `428 read_before_send_required`. Call `*_read_messages` on that chat first.
52
+ Email sends require a recent `email_read` whose thread contains the exact
53
+ recipient address.
41
54
  - **No double-send.** An identical message to the same chat returns
42
- `409 duplicate_send`.
55
+ `409 duplicate_send`. The same guard fingerprints email recipient, subject,
56
+ and body.
43
57
  - **Daily send cap + pacing.** Exceeding it returns `429 rate_limit_exceeded`.
44
58
 
45
59
  These are server-side guards, not suggestions. Do not try to work around them.
@@ -52,8 +66,9 @@ them from you sending something wrong. They are not optional.
52
66
  1. **Never send without the user's explicit approval of the exact recipient and
53
67
  exact text.** Draft it, show it, wait. Discussion is not approval.
54
68
  2. **Read the thread before you send.** Always `*_read_messages` on that exact
55
- chat first: check who it is, what they last said, and whether this was
56
- already sent. Never send blind. Never double-send.
69
+ chat or `email_read` on the exact mail thread first: check who it is, what
70
+ they last said, and whether this was already sent. Never send blind. Never
71
+ double-send.
57
72
  3. **Never bulk.** No mass messaging, no "message everyone who…", no bulk
58
73
  profile fetching. LinkedIn restricts accounts for exactly this. Hard ceiling
59
74
  is roughly 15-20 LinkedIn sends per day; the server enforces a daily cap and
@@ -65,6 +80,16 @@ them from you sending something wrong. They are not optional.
65
80
  6. **Personalize.** Read the thread and write a specific reply. Templated
66
81
  identical messages get reported as spam.
67
82
 
83
+ ## Email-specific rules
84
+
85
+ - `email_send` accepts exactly one recipient. Do not split bulk sends into
86
+ repeated calls.
87
+ - Obtain explicit approval for the exact recipient, subject, and body.
88
+ - Read the recipient's recent thread immediately before sending. If there is no
89
+ existing thread, do not send through SignalDash.
90
+ - Stop on provider authentication errors, HTTP 403, or HTTP 429. Do not retry an
91
+ ambiguous timeout until the mailbox and sent history have been re-read.
92
+
68
93
  ## Good vs bad requests
69
94
 
70
95
  Good: "list my unread LinkedIn chats and draft replies for my approval" ·
@@ -1,17 +1,19 @@
1
1
  ---
2
2
  name: signaldash-safe-usage
3
- description: Safely read and send LinkedIn or WhatsApp messages through the SignalDash MCP without triggering account restrictions or contacting the wrong person. Use whenever an agent uses li_list_chats, li_read_messages, li_send_message, wa_list_chats, wa_read_messages, or wa_send_message.
3
+ description: Safely read and send LinkedIn, WhatsApp, or email messages through the SignalDash MCP without triggering account restrictions or contacting the wrong person. Use whenever an agent uses li_list_chats, li_read_messages, li_send_message, wa_list_chats, wa_read_messages, wa_send_message, email_list, email_read, or email_send.
4
4
  ---
5
5
 
6
6
  # Use SignalDash safely
7
7
 
8
- Treat LinkedIn and WhatsApp as human accounts, not bulk messaging APIs.
8
+ Treat LinkedIn, WhatsApp, and email as human accounts, not bulk messaging APIs.
9
9
 
10
10
  ## Before every send
11
11
 
12
12
  1. Confirm the requested channel and exact chat.
13
- 2. Use `li_list_chats` or `wa_list_chats`; never fetch profiles in bulk.
14
- 3. Read at least the 10 most recent messages in the exact thread.
13
+ 2. Use `li_list_chats`, `wa_list_chats`, or `email_list`; never fetch profiles
14
+ or mailboxes in bulk.
15
+ 3. Read at least the 10 most recent messages in the exact thread with
16
+ `li_read_messages`, `wa_read_messages`, or `email_read`.
15
17
  4. Check the recipient, prior conversation, last inbound message, and whether
16
18
  the proposed text was already sent.
17
19
  5. Obtain explicit human approval for the exact recipient and exact text when
@@ -21,6 +23,18 @@ Treat LinkedIn and WhatsApp as human accounts, not bulk messaging APIs.
21
23
  Never infer a recipient from a partial name. Never send blind. Never retry a
22
24
  send after an ambiguous timeout without first reading the thread.
23
25
 
26
+ ## Email limits
27
+
28
+ - `email_send` accepts one recipient per call. Never loop to simulate a bulk
29
+ send.
30
+ - Approve and verify the exact recipient, subject, and body.
31
+ - The server requires a recent `email_read` containing that recipient address,
32
+ blocks exact duplicates, and counts the send against the persisted daily cap.
33
+ - If no prior thread exists with the recipient, do not send through SignalDash.
34
+ - Stop on authentication errors, HTTP 403, HTTP 429, bounces, or provider
35
+ warnings. Re-read the mailbox and sent history before resolving any ambiguous
36
+ send outcome.
37
+
24
38
  ## LinkedIn limits
25
39
 
26
40
  - Keep normal messaging at 15 to 20 sends per day. SignalDash hard-caps at 18