@floomhq/signaldash 0.10.0 → 0.11.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.
- package/README.md +14 -7
- package/bin/sd.mjs +67 -10
- package/package.json +1 -1
- package/skills/signaldash/SKILL.md +38 -13
- package/skills/signaldash-safe-usage/SKILL.md +18 -4
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# SignalDash
|
|
2
2
|
|
|
3
|
-
Secure LinkedIn and
|
|
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
|
|
7
|
-
|
|
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
|
|
23
|
-
|
|
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
|
|
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,8 +105,8 @@ 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
|
}
|
|
@@ -118,10 +118,18 @@ export async function cmdConnect(provider, dependencies = {}) {
|
|
|
118
118
|
process.exitCode = 1;
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
|
-
const { chalk, ora, open } = await ui();
|
|
121
|
+
const { chalk, ora, open: defaultOpen } = await ui();
|
|
122
|
+
const openUrl = dependencies.open || defaultOpen;
|
|
122
123
|
log("\n " + chalk.dim("Opening your browser. If it does not open, use this link:"));
|
|
123
124
|
log(" " + chalk.cyan(r.json.url) + "\n");
|
|
124
|
-
try { await
|
|
125
|
+
try { await openUrl(r.json.url); } catch {}
|
|
126
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY) && !dependencies.forcePoll) {
|
|
127
|
+
log("");
|
|
128
|
+
log(` ACTION REQUIRED (human): open the link above to connect ${provider}.`);
|
|
129
|
+
log(` Then run: npx @floomhq/signaldash connect ${provider}`);
|
|
130
|
+
log("");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
125
133
|
const spin = ora({ text: `Waiting for ${provider} authentication...`, indent: 2 }).start();
|
|
126
134
|
dependencies._spin = spin;
|
|
127
135
|
|
|
@@ -164,14 +172,56 @@ const TOOLS = [
|
|
|
164
172
|
{ name: "wa_list_chats", ch: "wa", action: "list_chats", description: "List your WhatsApp chats." },
|
|
165
173
|
{ name: "wa_read_messages", ch: "wa", action: "read", description: "Read messages in a WhatsApp chat. args: chat_id" },
|
|
166
174
|
{ name: "wa_send_message", ch: "wa", action: "send", description: "Send a WhatsApp message (rate-safe). args: chat_id, text" },
|
|
175
|
+
{
|
|
176
|
+
name: "email_list",
|
|
177
|
+
path: "/email/list",
|
|
178
|
+
description: "List the newest message from each recent email thread. args: limit",
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: { limit: { type: "integer", minimum: 1, maximum: 100 } },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "email_read",
|
|
186
|
+
path: "/email/read",
|
|
187
|
+
description: "Read an email thread before drafting or sending. args: thread_id, limit",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
thread_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
192
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
193
|
+
},
|
|
194
|
+
required: ["thread_id"],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "email_send",
|
|
199
|
+
path: "/email/send",
|
|
200
|
+
description: "Send one approved email after reading that recipient's thread. args: to, subject, body",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
to: {
|
|
205
|
+
type: "array",
|
|
206
|
+
items: { type: "string", format: "email" },
|
|
207
|
+
minItems: 1,
|
|
208
|
+
maxItems: 1,
|
|
209
|
+
},
|
|
210
|
+
subject: { type: "string", minLength: 1, maxLength: 998 },
|
|
211
|
+
body: { type: "string", minLength: 1, maxLength: 5000 },
|
|
212
|
+
},
|
|
213
|
+
required: ["to", "subject", "body"],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
167
216
|
{ name: "li_my_posts", path: "/li/posts", description: "List the user's own LinkedIn posts with engagement counts (reactions, comments, impressions). args: limit" },
|
|
168
217
|
{ 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
218
|
{ name: "li_post_comments", path: "/li/post_comments", description: "Comments on a post, with author. args: post_id, limit" },
|
|
170
219
|
{ 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
220
|
];
|
|
172
221
|
function mcpTool(name) {
|
|
173
|
-
|
|
174
|
-
|
|
222
|
+
const tool = TOOLS.find(t => t.name === name);
|
|
223
|
+
return { name, description: tool.description,
|
|
224
|
+
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
225
|
}
|
|
176
226
|
export async function runMcp(dependencies = {}) {
|
|
177
227
|
const input = dependencies.input || process.stdin;
|
|
@@ -187,7 +237,7 @@ export async function runMcp(dependencies = {}) {
|
|
|
187
237
|
else if (method === "tools/call") {
|
|
188
238
|
const t = TOOLS.find(x => x.name === params.name);
|
|
189
239
|
if (!t) { reply(id, null, { code: -32601, message: "unknown tool" }); continue; }
|
|
190
|
-
const r = await request(`/${t.ch}/${t.action}`, params.arguments || {});
|
|
240
|
+
const r = await request(t.path || `/${t.ch}/${t.action}`, params.arguments || {});
|
|
191
241
|
reply(id, { content: [{ type: "text", text: JSON.stringify(r.json) }], isError: r.status >= 300 });
|
|
192
242
|
} else if (id !== undefined) reply(id, {});
|
|
193
243
|
}
|
|
@@ -235,7 +285,14 @@ export async function cmdSetup(code, dependencies = {}) {
|
|
|
235
285
|
}
|
|
236
286
|
log("");
|
|
237
287
|
|
|
238
|
-
|
|
288
|
+
// An AGENT (Claude Code / Cursor) runs this with no TTY. Blocking on an
|
|
289
|
+
// interactive menu is why "nothing happened" for the first testers: the
|
|
290
|
+
// process stopped at a prompt nobody could answer. Non-interactive => connect
|
|
291
|
+
// both and print links the agent can relay to the human.
|
|
292
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
293
|
+
const choice = dependencies.choice ?? (!interactive
|
|
294
|
+
? ["linkedin", "whatsapp"]
|
|
295
|
+
: (await prompts({
|
|
239
296
|
type: "select",
|
|
240
297
|
name: "value",
|
|
241
298
|
message: "Which accounts do you want to connect?",
|
|
@@ -246,7 +303,7 @@ export async function cmdSetup(code, dependencies = {}) {
|
|
|
246
303
|
{ title: "Skip for now", value: [] },
|
|
247
304
|
],
|
|
248
305
|
initial: 0,
|
|
249
|
-
})).value;
|
|
306
|
+
})).value);
|
|
250
307
|
|
|
251
308
|
if (!choice || choice.length === 0) {
|
|
252
309
|
log("\n " + chalk.dim("Nothing connected. Run `signaldash connect whatsapp` or `connect linkedin` anytime."));
|
|
@@ -269,7 +326,7 @@ export async function cmdStatus(dependencies = {}) {
|
|
|
269
326
|
const request = dependencies.request || api;
|
|
270
327
|
log("");
|
|
271
328
|
log(" " + chalk.bold("SignalDash") + chalk.dim(" " + (cfg.backend || DEFAULT_BACKEND)));
|
|
272
|
-
for (const provider of ["linkedin", "whatsapp"]) {
|
|
329
|
+
for (const provider of ["linkedin", "whatsapp", "email"]) {
|
|
273
330
|
const r = await request(`/connect/${provider}/status`, undefined, { method: "GET" });
|
|
274
331
|
const ok = r.status === 200 && r.json.connected;
|
|
275
332
|
log(" " + (ok ? chalk.green("+") : chalk.dim("-")) + " " + provider.padEnd(9) +
|
package/package.json
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: signaldash
|
|
3
|
-
description: Give this agent secure access to the user's LinkedIn
|
|
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
|
|
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 (
|
|
12
|
+
## Setup (run this when the SignalDash tools are not available yet)
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
24
|
-
`
|
|
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
|
|
56
|
-
already sent. Never send blind. Never
|
|
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
|
|
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
|
|
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 `
|
|
14
|
-
|
|
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
|