@floomhq/signaldash 0.12.0 → 0.22.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 +121 -12
- package/bin/sd.mjs +397 -27
- package/lib/cli.js +16 -22
- package/lib/config-file.js +114 -0
- package/package.json +3 -3
- package/skills/signaldash/SKILL.md +1250 -95
- package/skills/signaldash-safe-usage/SKILL.md +74 -9
|
@@ -1,107 +1,1262 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: signaldash
|
|
3
|
-
description:
|
|
3
|
+
description: Operate the user's SignalDash connection to LinkedIn, WhatsApp, and email. Use this skill whenever the user says or implies "check my LinkedIn", "who messaged me", "triage my inbox", "read the thread with X", "reply to X", "send this message", "send a connection request", "create an invitation batch", "check invitations", "accept this invitation", "auto-accept my invitations", "check auto-accept status", "withdraw this invitation", "search my LinkedIn connections", "find people in my network", "find people beyond my network", "suppress this contact", "check contact state", "check my action budget", "check WhatsApp", "check my email", "draft a LinkedIn post", "who engaged with my post", "who liked or commented", or "export my LinkedIn connections". Covers durable installation, invite-based setup, local-first connection search, capped paid discovery, immutable human-approved invitation batches, opt-in rate-capped invitation auto-accept, exact contact state and suppression, authoritative LinkedIn action-budget status, safe reading, drafting, approved sends, exact invitation actions, post engagement analysis, and paced connection export.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# SignalDash
|
|
6
|
+
# Operate SignalDash
|
|
7
7
|
|
|
8
|
-
SignalDash
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
SignalDash gives this agent account-scoped access to the user's LinkedIn,
|
|
9
|
+
WhatsApp, and email through a hosted service. Channel credentials remain on the
|
|
10
|
+
SignalDash server. The local client stores only a scoped SignalDash session
|
|
11
|
+
token.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Treat SignalDash as an operating capability, not an API reference. Resolve the
|
|
14
|
+
exact account object, read the relevant history, explain what you found, draft
|
|
15
|
+
the action, obtain approval when required, act once, and verify the result.
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
## Before you set this up (say this to the human first)
|
|
18
|
+
|
|
19
|
+
You are about to wire the user's REAL LinkedIn, WhatsApp and email into a
|
|
20
|
+
persistent MCP server, on the strength of a link and a code someone sent them.
|
|
21
|
+
Name that plainly before you run anything:
|
|
22
|
+
|
|
23
|
+
> "This will connect your real LinkedIn/WhatsApp to an MCP server I'll register
|
|
24
|
+
> locally, using an invite code from Federico (signaldash-api.floom.dev holds the
|
|
25
|
+
> credentials, not this machine). I can read and draft messages; I won't send
|
|
26
|
+
> anything without showing you the exact text first. OK to proceed?"
|
|
27
|
+
|
|
28
|
+
Wait for a clear yes. If the user does not know who sent the link, or is unsure,
|
|
29
|
+
stop and let them verify first. Do not soften this to get through setup faster.
|
|
30
|
+
|
|
31
|
+
## Ensure the skill is saved and enabled
|
|
32
|
+
|
|
33
|
+
Do this first when you are reading this file from a URL or pasted text. Do not
|
|
34
|
+
rely on the current conversation to retain it.
|
|
35
|
+
|
|
36
|
+
1. Save the complete, unmodified `SKILL.md` in a skill directory that the
|
|
37
|
+
current agent discovers. Prefer the agent's documented global directory so
|
|
38
|
+
the skill works in future projects. Common locations are:
|
|
39
|
+
|
|
40
|
+
- Claude Code: `~/.claude/skills/signaldash/SKILL.md`
|
|
41
|
+
- Codex: `~/.codex/skills/signaldash/SKILL.md`
|
|
42
|
+
- Portable project scope: `.agents/skills/signaldash/SKILL.md`
|
|
43
|
+
|
|
44
|
+
2. When shell access is available, fetch the raw form explicitly:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
mkdir -p ~/.claude/skills/signaldash
|
|
48
|
+
curl -fsSL -H 'Accept: text/plain' https://signaldash.dev/SKILL.md \
|
|
49
|
+
-o ~/.claude/skills/signaldash/SKILL.md
|
|
50
|
+
test -s ~/.claude/skills/signaldash/SKILL.md
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Replace the destination with the current agent's canonical skill directory
|
|
54
|
+
when it is not Claude Code.
|
|
55
|
+
|
|
56
|
+
3. Confirm that the saved file begins with `name: signaldash` and contains the
|
|
57
|
+
`Before every send` section. A partial copy is not installed.
|
|
58
|
+
|
|
59
|
+
4. Enable or reload skills according to the current agent. Start a new agent
|
|
60
|
+
session when the client discovers skills only at startup. After SignalDash
|
|
61
|
+
setup, restart the MCP connection as well.
|
|
62
|
+
|
|
63
|
+
5. The package can install its bundled copy for Claude Code:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx -y @floomhq/signaldash skill
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Expected success:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
Installed the SignalDash skill to /home/user/.claude/skills/signaldash/SKILL.md
|
|
73
|
+
Your agent now knows how to use LinkedIn + WhatsApp safely through SignalDash.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Persisting the skill is part of setup. Do not continue with a session-only copy
|
|
77
|
+
and tell the user setup is complete.
|
|
78
|
+
|
|
79
|
+
## When to reach for SignalDash
|
|
80
|
+
|
|
81
|
+
Use SignalDash without waiting for the user to name it when their request
|
|
82
|
+
involves their connected messaging accounts or their own LinkedIn activity.
|
|
83
|
+
Typical trigger phrases include:
|
|
84
|
+
|
|
85
|
+
- "Check my LinkedIn", "who messaged me?", "anything unread?", "triage my
|
|
86
|
+
inbox", or "what needs a reply?"
|
|
87
|
+
- "Read the thread with Amina", "reply to Marco", "send this on LinkedIn", or
|
|
88
|
+
"follow up with the person who asked about pricing."
|
|
89
|
+
- "Check WhatsApp", "what did the team say?", "find my chat with Sara", or
|
|
90
|
+
"reply with this."
|
|
91
|
+
- "Check my email", "summarize the thread", "draft a response", or "send the
|
|
92
|
+
approved email."
|
|
93
|
+
- "Draft a post", "turn this into a LinkedIn post", or "publish this post."
|
|
94
|
+
- "Send a connection request to this exact person", "check my invitations",
|
|
95
|
+
"accept this invitation", or "withdraw this pending invitation."
|
|
96
|
+
- "How many LinkedIn actions remain?", "check my action budget", or "can a
|
|
97
|
+
campaign act today?"
|
|
98
|
+
- "Auto-accept my LinkedIn invitations", "turn invitation auto-accept off", or
|
|
99
|
+
"did auto-accept hit any errors?" Use the persistent per-user setting and
|
|
100
|
+
status tools; it is off until explicitly enabled.
|
|
101
|
+
- "Search my LinkedIn connections", "find founders in my network", or "who at
|
|
102
|
+
Acme am I connected to?" Use the local connection snapshot before any
|
|
103
|
+
external or paid discovery.
|
|
104
|
+
- "Find platform engineers beyond my network" or "discover people at Acme in
|
|
105
|
+
Berlin." Use `li_discover_people` only after the completed local snapshot
|
|
106
|
+
returns no matches. Preview the exact paid request and cost before confirming
|
|
107
|
+
one capped provider page.
|
|
108
|
+
- "Schedule this message", "follow up automatically", or "send these as a
|
|
109
|
+
sequence." Explain the narrow campaign time boundary below; do not invent a
|
|
110
|
+
queue or claim those unsupported actions were scheduled.
|
|
111
|
+
- "Create a LinkedIn invitation batch for these exact profiles." Structure the
|
|
112
|
+
human's request into exact URLs, reasons, and notes, create the immutable
|
|
113
|
+
preview, and relay its human review path. Never approve through MCP.
|
|
114
|
+
- "Who engaged with my last post?", "who liked it?", "what did people comment?",
|
|
115
|
+
or "which warm signals need action?"
|
|
116
|
+
- "Export my connections", "download my LinkedIn network", or "make me a
|
|
117
|
+
connections CSV."
|
|
118
|
+
|
|
119
|
+
Do not use SignalDash for general public LinkedIn research, full-profile
|
|
120
|
+
enrichment, email finding, unreviewed bulk outreach, unapproved automatic
|
|
121
|
+
invitation processing, or a new email to someone with no existing thread.
|
|
122
|
+
`li_discover_people` is the narrow exception for one capped page of public
|
|
123
|
+
search cards after own-network search. Its results are planning evidence only.
|
|
124
|
+
Invitation tools act on one exact provider member or invitation at a time and
|
|
125
|
+
never authorize a list-wide loop. The only automatic incoming-invitation
|
|
126
|
+
exception is the explicitly enabled, filterable, rate-capped auto-accept worker.
|
|
127
|
+
The only multi-target outbound exception is an immutable 1–10-target invitation
|
|
128
|
+
batch with separate exact browser approval and server-controlled execution.
|
|
129
|
+
|
|
130
|
+
## The operating model
|
|
131
|
+
|
|
132
|
+
There are two surfaces:
|
|
133
|
+
|
|
134
|
+
1. The CLI handles login, account connection, status, skill installation, MCP
|
|
135
|
+
startup, logout, and the paced LinkedIn connections export.
|
|
136
|
+
2. MCP tools handle account reads, local connection search, capped paid
|
|
137
|
+
discovery, message sends, immutable invitation-batch create/inspect/cancel,
|
|
138
|
+
exact LinkedIn invitation actions, email, LinkedIn posts, and post
|
|
139
|
+
engagement.
|
|
140
|
+
|
|
141
|
+
The CLI command used by the MCP registration is:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npx -y @floomhq/signaldash mcp
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
MCP tool results arrive as JSON text. An MCP result marked `isError: true`, an
|
|
148
|
+
HTTP-style `code`, or an `error` field is not a successful action.
|
|
149
|
+
|
|
150
|
+
## First-time setup
|
|
151
|
+
|
|
152
|
+
The user needs a single-use SignalDash invite code from Federico. Ask the user
|
|
153
|
+
for that code when none is present. Never invent one, search private files for
|
|
154
|
+
one, or reuse somebody else's code.
|
|
155
|
+
|
|
156
|
+
### 1. Run the one-command setup
|
|
157
|
+
|
|
158
|
+
After the user provides the code, run:
|
|
16
159
|
|
|
17
160
|
```bash
|
|
18
161
|
npx -y @floomhq/signaldash <invite-code>
|
|
19
162
|
```
|
|
20
163
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
164
|
+
The invite is a hexadecimal string. The command logs in, installs the bundled
|
|
165
|
+
skill, registers the MCP server with Claude Code when available, and begins
|
|
166
|
+
LinkedIn and WhatsApp connection.
|
|
167
|
+
|
|
168
|
+
Expected initial output resembles:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
SignalDash secure LinkedIn + WhatsApp access for your agent
|
|
172
|
+
|
|
173
|
+
+ logged in
|
|
174
|
+
+ agent skill installed
|
|
175
|
+
+ MCP registered with Claude Code
|
|
176
|
+
|
|
177
|
+
Opening your browser. If it does not open, use this link:
|
|
178
|
+
https://account.unipile.com/...
|
|
179
|
+
|
|
180
|
+
ACTION REQUIRED (human): open the link above to connect linkedin.
|
|
181
|
+
Then run: npx @floomhq/signaldash connect linkedin
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
In a non-interactive agent shell, the command prints hosted-auth links. The
|
|
185
|
+
agent cannot complete those pages, sign in as the user, or scan a WhatsApp QR
|
|
186
|
+
code.
|
|
187
|
+
|
|
188
|
+
### 2. Relay every printed link to the human
|
|
189
|
+
|
|
190
|
+
Tell the user which provider each link connects and what human action it needs:
|
|
191
|
+
|
|
192
|
+
- LinkedIn: open the hosted-auth link and finish LinkedIn authentication.
|
|
193
|
+
- WhatsApp: open the link and scan the live QR code from WhatsApp > Linked
|
|
194
|
+
Devices.
|
|
195
|
+
- Email: run `npx -y @floomhq/signaldash connect email`, relay its link, and let
|
|
196
|
+
the human choose Google, Outlook, or IMAP when available.
|
|
197
|
+
|
|
198
|
+
Do not hide a link in a summary. Send the full URL in a clickable form. Do not
|
|
199
|
+
click, scan, enter passwords, or claim connection on the user's behalf.
|
|
200
|
+
|
|
201
|
+
### 3. Verify with status
|
|
202
|
+
|
|
203
|
+
After the human says authentication is complete, run:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npx -y @floomhq/signaldash status
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`status` is the source of truth after every `connect` attempt.
|
|
210
|
+
|
|
211
|
+
Connected output:
|
|
212
|
+
|
|
213
|
+
```text
|
|
214
|
+
SignalDash https://signaldash-api.floom.dev
|
|
215
|
+
+ linkedin Federico De Ponte
|
|
216
|
+
+ whatsapp Federico
|
|
217
|
+
- email not connected
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Not-connected output:
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
SignalDash https://signaldash-api.floom.dev
|
|
224
|
+
- linkedin not connected
|
|
225
|
+
- whatsapp not connected
|
|
226
|
+
- email not connected
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
A `connect` command printing another hosted-auth URL, even one that looks
|
|
230
|
+
identical to the previous output, does not prove the provider is connected.
|
|
231
|
+
Only the `+ <provider> <name>` line from `status`, or a direct
|
|
232
|
+
`Connected <provider>: <name>` result, proves connection.
|
|
233
|
+
|
|
234
|
+
### 4. Continue or diagnose each provider
|
|
235
|
+
|
|
236
|
+
Note: the one-command setup connects LinkedIn and WhatsApp. Email is opt-in and
|
|
237
|
+
needs its own call: `npx -y @floomhq/signaldash connect email`.
|
|
238
|
+
|
|
239
|
+
Run a missing provider explicitly:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npx -y @floomhq/signaldash connect linkedin
|
|
243
|
+
npx -y @floomhq/signaldash connect whatsapp
|
|
244
|
+
npx -y @floomhq/signaldash connect email
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Interpret output as follows:
|
|
248
|
+
|
|
249
|
+
- Success: `Connected linkedin: <name>` or `status` shows `+ linkedin`.
|
|
250
|
+
- Pending human action: a hosted-auth URL plus `ACTION REQUIRED (human)`.
|
|
251
|
+
Relay the URL and wait for the human to finish.
|
|
252
|
+
- Still pending in an interactive terminal: `Not connected yet. The link above
|
|
253
|
+
stays valid...`. Do not treat the timeout as a failed login and do not create
|
|
254
|
+
a rapid loop of new links.
|
|
255
|
+
- Login failure: `Not logged in. Run: signaldash login <invite-code>` or
|
|
256
|
+
`login failed: ...`. Stop and resolve the session or invite.
|
|
257
|
+
- Provider failure: `connect failed: ...` or `connect status failed: ...`.
|
|
258
|
+
Report the exact error without claiming connection.
|
|
259
|
+
|
|
260
|
+
If automatic account detection cannot bind the newly authenticated account,
|
|
261
|
+
the CLI prints this manual fallback:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
npx -y @floomhq/signaldash connect linkedin claim <account_id>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Use `claim` only with the exact account ID produced for this user's just-created
|
|
268
|
+
connection. Never guess an account ID.
|
|
269
|
+
|
|
270
|
+
### 5. Reload and prove tool availability
|
|
271
|
+
|
|
272
|
+
Restart the MCP connection or agent session after registration. Confirm the
|
|
273
|
+
expected SignalDash tools are present, then perform a read-only check such as:
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
li_list_chats({"limit": 5})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
If the tools are missing but `status` is connected, the account is ready and
|
|
280
|
+
the MCP client is not loaded. Fix the MCP registration or restart the client;
|
|
281
|
+
do not reconnect the account.
|
|
282
|
+
|
|
283
|
+
## Numbered workflow for every account task
|
|
284
|
+
|
|
285
|
+
Use this sequence unless the request is read-only and ends before approval:
|
|
286
|
+
|
|
287
|
+
1. **Confirm connection.** Run `npx -y @floomhq/signaldash status` when channel
|
|
288
|
+
state is unknown. A connected status is required.
|
|
289
|
+
2. **Choose the narrowest list or preview tool.** List recent chats, email
|
|
290
|
+
threads, invitation inboxes, or the user's recent posts. Preview an outbound
|
|
291
|
+
invitation through `li_send_invitation` with `confirm` omitted. Use a modest
|
|
292
|
+
limit.
|
|
293
|
+
3. **Resolve the exact object.** Match the full chat name, thread, invitation,
|
|
294
|
+
provider member, or post.
|
|
295
|
+
When names collide or identity is unclear, show the candidates and ask the
|
|
296
|
+
user. Never infer from a partial name.
|
|
297
|
+
4. **Read before interpreting.** Read enough recent history to understand the
|
|
298
|
+
latest inbound message, earlier context, and existing outbound messages.
|
|
299
|
+
For message sends, read at least 10 recent items and use the exact chat or
|
|
300
|
+
thread. For invitation maintenance, list the exact current invitation. For
|
|
301
|
+
a new invitation, inspect the server-confirmed exact target and note preview.
|
|
302
|
+
5. **Return findings or draft.** Summarize concrete facts. Separate suggested
|
|
303
|
+
replies from messages already sent.
|
|
304
|
+
6. **Obtain explicit approval.** Before any message or public post, show the
|
|
305
|
+
exact channel, recipient, and complete text. For email include subject and
|
|
306
|
+
body. Discussion, editing, "looks good", or approval of a different draft is
|
|
307
|
+
not approval of the final action.
|
|
308
|
+
7. **Re-read immediately before acting.** Re-read the exact thread or current
|
|
309
|
+
invitation list to catch a human action, reply, duplicate, acceptance, or
|
|
310
|
+
withdrawal that appeared after drafting. New invitation sends repeat the
|
|
311
|
+
exact previewed payload with `confirm:true`; the server rechecks relationship
|
|
312
|
+
and pending invitation state.
|
|
313
|
+
8. **Act once.** Send one approved message or publish one approved post. Never
|
|
314
|
+
parallelize sends and never loop over recipients.
|
|
315
|
+
9. **Verify.** Read the exact thread again after a successful send. Confirm the
|
|
316
|
+
outbound text is present once. If the outcome was ambiguous, re-read before
|
|
317
|
+
any retry.
|
|
318
|
+
10. **Report guard state.** Include rate-limit remaining/reset information when
|
|
319
|
+
returned. Stop on warnings, 403, or 429.
|
|
320
|
+
|
|
321
|
+
Expected states:
|
|
322
|
+
|
|
323
|
+
- Read/list success: JSON containing `items`, often with a `cursor`.
|
|
324
|
+
- Draft success: `{"drafted":true,"published":false,...}`.
|
|
325
|
+
- Send success: provider result plus `rate_limit`, often including a message ID.
|
|
326
|
+
- Validation failure: `invalid_request` or a clear required-argument error.
|
|
327
|
+
- Safety rejection: 428, 409, or 429 as documented below.
|
|
328
|
+
- Provider warning/restriction: stop all activity on that account.
|
|
329
|
+
|
|
330
|
+
## Where your session lives
|
|
331
|
+
|
|
332
|
+
The CLI stores a SignalDash session token at `~/.signaldash/config.json`
|
|
333
|
+
(mode 0600). It is a scoped SignalDash token, NOT a LinkedIn/WhatsApp/email
|
|
334
|
+
credential: those stay on the server. Override the location with
|
|
335
|
+
`SIGNALDASH_HOME=/path` (useful for sandboxing or multiple accounts). Revoke a
|
|
336
|
+
device with `npx -y @floomhq/signaldash logout`.
|
|
337
|
+
|
|
338
|
+
## Tool catalog
|
|
339
|
+
|
|
340
|
+
Use the exact tool names and argument keys below. Limits are optional.
|
|
341
|
+
|
|
342
|
+
| Tool | Arguments | When to use it |
|
|
343
|
+
|---|---|---|
|
|
344
|
+
| `li_list_chats` | `limit` integer 1-100, default 20 | Find recent LinkedIn chats, unread counts, and exact `chat_id` values without opening profiles. |
|
|
345
|
+
| `li_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Read one resolved LinkedIn conversation before summarizing, drafting, or sending. |
|
|
346
|
+
| `li_send_message` | `chat_id` required; `text` required, max 5000 characters | Send one approved LinkedIn reply after an immediate read of that exact chat. |
|
|
347
|
+
| `li_send_invitation` | `provider_id` required; `note` optional, max 300 exact characters; `confirm` optional, default false | First preview one exact target and note. The server verifies relationship, both invitation directions, and absence of an existing one-to-one chat. After exact approval, repeat the identical call with `confirm:true`; jitter completes before the final preflight and action reservation. |
|
|
348
|
+
| `li_invitations_received` | `limit` integer 1-100, default 50; `cursor` optional | List one bounded page of received invitations. This read authorizes only the exact returned invitation IDs for a later accept. |
|
|
349
|
+
| `li_accept_invitation` | `invitation_id` required; `confirm:true` required | Accept one exact currently pending received invitation after a fresh `li_invitations_received` read and approval. |
|
|
350
|
+
| `li_invitations_sent` | `limit` integer 1-100, default 50; `cursor` optional | List one bounded page of sent invitations. This read authorizes only the exact returned invitation IDs for a later withdrawal. |
|
|
351
|
+
| `li_withdraw_invitation` | `invitation_id` required; `confirm:true` required | Withdraw one exact currently pending sent invitation after a fresh `li_invitations_sent` read and approval. |
|
|
352
|
+
| `sd_contact_state` | `channel` required (`linkedin`, `whatsapp`, or `email`); `identifiers` required array of 1-8 exact `{kind,value}` objects; `action` optional (`get` default or `suppress`); suppression also requires an allowed `reason` and `confirm:true` | Inspect exact tenant/channel-scoped contact history or add a protective suppression. It never infers that identifiers on different channels belong to one person. |
|
|
353
|
+
| `sd_budget_status` | no arguments | Read the current sender binding, daily total/manual/campaign/unknown attempts, total and combined-campaign capacity, weekly invitation usage, lock state, and UTC resets from the authoritative server ledger. |
|
|
354
|
+
| `sd_settings_get` | no arguments | Read the authenticated user's persistent SignalDash settings. Auto-accept is the first supported setting and is false for every existing user until explicitly changed. |
|
|
355
|
+
| `sd_settings_set` | `auto_accept_linkedin` required boolean; `auto_accept_linkedin_filters` optional object with `public_identifiers` and `description_keywords`; `confirm:true` required | Update the general per-user settings surface after exact human approval. This changes no future or unknown setting implicitly. |
|
|
356
|
+
| `sd_auto_accept_status` | no arguments | Inspect whether auto-accept is enabled, its exact filters, accepted today and this week, failed attempts today, dedicated and shared capacity, repeated-error stop state, and sanitized unparseable invitation records. |
|
|
357
|
+
| `li_search_connections` | `query` required string, max 200; `filters` optional object with `company`, `headline_keyword`, `connected_after`, and `connected_before`; `limit` integer 1-100, default 20 | Search only the authenticated user's stored LinkedIn connection snapshot and join exact local contact state. This makes no LinkedIn, Unipile, HarvestAPI, or other paid discovery call. |
|
|
358
|
+
| `li_discover_people` | `query` required role/title string, max 200; `filters` optional object with `company` and `location`; `limit` integer 1-10, default 10; `confirm` optional, default false | After a completed local snapshot returns no matches, preview one paid HarvestAPI profile-search page. Obtain approval for the exact query and maximum reserved cost, then repeat with `confirm:true`. |
|
|
359
|
+
| `li_create_invitation_batch` | `source_label` required exact string, max 120 code points; `time_zone` required IANA timezone; `targets` required array of 1-10 exact `{profile_url,inclusion_reason,note?}` objects; reason max 240 and note max 200 Unicode code points | Create one immutable durable preview from canonical LinkedIn Classic `/in/` URLs. The agent structures an explicit human request; the server never generates targets or text. |
|
|
360
|
+
| `li_get_invitation_batch` | `batch_id` required | Inspect every stored target, exact note, exclusion, hash, timing, capacity, and result. Use the returned browser review path for human approval. This read also authorizes a later exact cancel. |
|
|
361
|
+
| `li_cancel_invitation_batch` | `batch_id` required; `approval_view_hash` required string or null exactly as inspected; `confirm:true` required | Permanently cancel unstarted targets in one freshly inspected batch. It cannot recall an executing invitation, and restarting requires a new preview and approval. |
|
|
362
|
+
| `wa_list_chats` | `limit` integer 1-100, default 20 | Find an existing WhatsApp conversation and exact `chat_id`. |
|
|
363
|
+
| `wa_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Verify a WhatsApp contact and recent history before summarizing, drafting, or sending. |
|
|
364
|
+
| `wa_send_message` | `chat_id` required; `text` required, max 5000 characters | Send one approved reply in an existing WhatsApp conversation after an immediate re-read. |
|
|
365
|
+
| `email_list` | `limit` integer 1-100, default 20 | List the newest message in each recent email thread and obtain `thread_id`. |
|
|
366
|
+
| `email_read` | `thread_id` required; `limit` 1-100, default 30 | Read an email thread and authorize its exact participant addresses for a later send. |
|
|
367
|
+
| `email_send` | `to` required as an array of exactly one valid address; `subject` required, max 998; `body` required, max 5000 | Send one approved email to a participant in a recently read existing thread. |
|
|
368
|
+
| `li_my_posts` | `limit` default 10, max 50; `member_id` optional | Find the user's latest posts and post IDs. Omit `member_id` to use the connected user's own ID. |
|
|
369
|
+
| `li_post_reactions` | `post_id` required; `limit` default 50, max 100 | Identify who reacted to one post and assess warm signals. A reaction does not authorize outreach. |
|
|
370
|
+
| `li_post_comments` | `post_id` required; `limit` default 50, max 100 | Read comments and authors for one post; prioritize questions and substantive responses. |
|
|
371
|
+
| `li_draft_post` | `text` required, max 3000; `publish` optional, default false | Create a server-confirmed draft. Use `publish:true` only after explicit approval of the final public text. |
|
|
372
|
+
|
|
373
|
+
Representative calls:
|
|
374
|
+
|
|
375
|
+
```text
|
|
376
|
+
li_list_chats({"limit":20})
|
|
377
|
+
li_read_messages({"chat_id":"chat_li_7f3a","limit":20})
|
|
378
|
+
li_send_message({"chat_id":"chat_li_7f3a","text":"Yes. I’ll send it this afternoon."})
|
|
379
|
+
li_send_invitation({"provider_id":"ACoAAExactMember","note":"Hi Amina, I enjoyed your post on agent safety."})
|
|
380
|
+
li_send_invitation({"provider_id":"ACoAAExactMember","note":"Hi Amina, I enjoyed your post on agent safety.","confirm":true})
|
|
381
|
+
li_invitations_received({"limit":20})
|
|
382
|
+
li_accept_invitation({"invitation_id":"invite_received_42","confirm":true})
|
|
383
|
+
li_invitations_sent({"limit":20})
|
|
384
|
+
li_withdraw_invitation({"invitation_id":"invite_sent_91","confirm":true})
|
|
385
|
+
sd_contact_state({"channel":"linkedin","identifiers":[{"kind":"provider_id","value":"ACoAAExactMember"}]})
|
|
386
|
+
sd_contact_state({"channel":"email","identifiers":[{"kind":"email","value":"amina@example.com"}],"action":"suppress","reason":"opt_out","confirm":true})
|
|
387
|
+
sd_budget_status({})
|
|
388
|
+
sd_settings_get({})
|
|
389
|
+
sd_settings_set({"auto_accept_linkedin":true,"auto_accept_linkedin_filters":{"public_identifiers":["amina-rahman"],"description_keywords":["Founder"]},"confirm":true})
|
|
390
|
+
sd_auto_accept_status({})
|
|
391
|
+
li_search_connections({"query":"founder agents","filters":{"company":"Acme","headline_keyword":"Founder","connected_after":"2025-01-01","connected_before":"2026-01-01"},"limit":20})
|
|
392
|
+
li_discover_people({"query":"Platform Engineer","filters":{"company":"Acme","location":"Berlin"},"limit":5})
|
|
393
|
+
li_discover_people({"query":"Platform Engineer","filters":{"company":"Acme","location":"Berlin"},"limit":5,"confirm":true})
|
|
394
|
+
li_create_invitation_batch({"source_label":"Approved Berlin founder shortlist","time_zone":"Europe/Berlin","targets":[{"profile_url":"https://www.linkedin.com/in/amina-rahman/","inclusion_reason":"Named by the user for this exact batch","note":"Hi Amina, I enjoyed your post on agent safety."}]})
|
|
395
|
+
li_get_invitation_batch({"batch_id":"00000000-0000-4000-8000-000000000000"})
|
|
396
|
+
li_cancel_invitation_batch({"batch_id":"00000000-0000-4000-8000-000000000000","approval_view_hash":"exact-64-character-hash-from-the-read","confirm":true})
|
|
397
|
+
wa_list_chats({"limit":20})
|
|
398
|
+
wa_read_messages({"chat_id":"chat_wa_91b2","limit":20})
|
|
399
|
+
wa_send_message({"chat_id":"chat_wa_91b2","text":"16:30 works. See you then."})
|
|
400
|
+
email_list({"limit":20})
|
|
401
|
+
email_read({"thread_id":"thread_email_c402","limit":30})
|
|
402
|
+
email_send({"to":["amina@example.com"],"subject":"Re: Case study","body":"Hi Amina,\n\nHere is the case study."})
|
|
403
|
+
li_my_posts({"limit":5})
|
|
404
|
+
li_post_reactions({"post_id":"post_urn_8821","limit":50})
|
|
405
|
+
li_post_comments({"post_id":"post_urn_8821","limit":50})
|
|
406
|
+
li_draft_post({"text":"Most agents need better context, not more autonomy."})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
List and read success returns JSON with `items` and often a `cursor`. A message
|
|
410
|
+
send success returns a provider message ID and `rate_limit` data. The draft
|
|
411
|
+
result is deterministic:
|
|
412
|
+
|
|
413
|
+
```json
|
|
414
|
+
{
|
|
415
|
+
"drafted": true,
|
|
416
|
+
"published": false,
|
|
417
|
+
"text": "Most agents need better context, not more autonomy.",
|
|
418
|
+
"note": "Draft only. Re-send with publish:true after the human approves."
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Publishing is public and irreversible. Never interpret "draft a post" as
|
|
423
|
+
permission to publish. `email_send` cannot start a cold thread and must never be
|
|
424
|
+
looped over recipients.
|
|
425
|
+
|
|
426
|
+
### Exact contact state and suppression
|
|
427
|
+
|
|
428
|
+
`sd_contact_state` is local and account-scoped. It makes no Unipile, LinkedIn,
|
|
429
|
+
WhatsApp, or email provider request. Exact identifiers are keyed separately by
|
|
430
|
+
user and channel, and the response does not echo their values.
|
|
431
|
+
|
|
432
|
+
Allowed identifier kinds:
|
|
433
|
+
|
|
434
|
+
- LinkedIn: `provider_id`, `public_identifier`, canonical `profile_url`,
|
|
435
|
+
`member_urn`, or `chat_id`.
|
|
436
|
+
- WhatsApp: `chat_id` or an E.164 `phone`.
|
|
437
|
+
- Email: normalized `email`.
|
|
438
|
+
|
|
439
|
+
One fresh provider response can supply several exact identifiers for the same
|
|
440
|
+
channel. Pass those together to get the conservative union of their state.
|
|
441
|
+
Never combine identifiers based on a name, employer, headline, similar profile,
|
|
442
|
+
phone guess, or email guess. SignalDash does not infer cross-channel identity
|
|
443
|
+
and does not claim person-wide suppression.
|
|
444
|
+
|
|
445
|
+
Use `action:"get"` before outreach planning. `campaign_eligible:false` and
|
|
446
|
+
`blocking_reasons` report known replies, earlier outbound touches,
|
|
447
|
+
already-connected state, or suppression. Automated campaign actions are
|
|
448
|
+
rejected after a known reply or earlier touch. Manual approved replies remain
|
|
449
|
+
available unless an exact identifier is suppressed.
|
|
450
|
+
|
|
451
|
+
Use `action:"suppress"` only after the human explicitly asks to suppress the
|
|
452
|
+
exact identifier. Show the channel, identifier kind, and reason, then repeat
|
|
453
|
+
with `confirm:true`. Allowed reasons are `opt_out`, `not_interested`,
|
|
454
|
+
`provider_block`, `manual`, and `legal`. MCP cannot clear a suppression.
|
|
455
|
+
|
|
456
|
+
### LinkedIn action budget status
|
|
457
|
+
|
|
458
|
+
`sd_budget_status` is read-only and account-scoped. Use it before planning
|
|
459
|
+
manual LinkedIn work or any campaign action. It reports attempts that
|
|
460
|
+
SignalDash has committed in its authoritative SQLite ledger:
|
|
461
|
+
|
|
462
|
+
- the total daily cap (`H`);
|
|
463
|
+
- the campaign-excluded portion (`R = min(4, H)`);
|
|
464
|
+
- the combined ceiling across all campaigns (`C = H - R`);
|
|
465
|
+
- total, manual, campaign, and unknown attempts for the UTC policy day;
|
|
466
|
+
- raw total and combined-campaign capacity remaining;
|
|
467
|
+
- actions available now after connection and sender-lock state;
|
|
468
|
+
- weekly LinkedIn invitation usage and its Monday 00:00 UTC reset.
|
|
469
|
+
|
|
470
|
+
`R` is capacity campaigns cannot consume. It is not a reserve or guaranteed
|
|
471
|
+
manual allowance: manual activity can consume the total cap first, and a later
|
|
472
|
+
manual action can still be rejected at `H`. Campaign capacity is shared across
|
|
473
|
+
all campaigns and is additionally bounded by the total remaining capacity.
|
|
474
|
+
|
|
475
|
+
The result counts SignalDash-recorded attempts only. Native LinkedIn activity
|
|
476
|
+
is not fully counted, so never describe the result as a LinkedIn-safe
|
|
477
|
+
allowance. A disconnected, unverified, or locked sender reports zero safe
|
|
478
|
+
actions even when raw counter capacity remains. It reports the durable pacing
|
|
479
|
+
time and work window after a campaign action has established them. The
|
|
480
|
+
scheduler, persisted tenant-breaker, active-batch, and target-count fields
|
|
481
|
+
reflect the current invitation-batch runtime.
|
|
482
|
+
|
|
483
|
+
### Opt-in invitation auto-accept
|
|
484
|
+
|
|
485
|
+
`sd_settings_get` is the single read surface for persistent SignalDash
|
|
486
|
+
settings. `sd_settings_set` changes persistent behavior for only the
|
|
487
|
+
authenticated SignalDash user. Auto-accept is the first supported setting;
|
|
488
|
+
every existing and newly created user is disabled by default. Show the exact
|
|
489
|
+
setting and filters, obtain explicit human approval, then call the setter once
|
|
490
|
+
with `confirm:true`. Disabling also uses `confirm:true`. Never infer changes to
|
|
491
|
+
future or unknown settings.
|
|
492
|
+
|
|
493
|
+
With no filters, every parseable pending LinkedIn invitation is eligible. The
|
|
494
|
+
optional `public_identifiers` list is an exact lowercase allowlist. Optional
|
|
495
|
+
`description_keywords` are case- and accent-insensitive substring matches
|
|
496
|
+
against the provider's inviter description. When both groups are present, both
|
|
497
|
+
must match. Names are never used as identity filters.
|
|
498
|
+
|
|
499
|
+
The server worker is the action authority. By default it reads one bounded
|
|
500
|
+
page, accepts no more than five invitations per run and ten per UTC day,
|
|
501
|
+
jitters before every fresh preflight, serializes writes per sender, consumes
|
|
502
|
+
the shared LinkedIn daily budget, and stops on a warning, 403, 429, unknown
|
|
503
|
+
outcome, sender lock, or three consecutive errors. Successful acceptance is
|
|
504
|
+
written to the action audit and exact LinkedIn contact state as inbound,
|
|
505
|
+
invited, and connected, so later campaigns see the relationship.
|
|
506
|
+
A healthy worker run resets the consecutive provider-error streak; malformed
|
|
507
|
+
invitations remain visible in status even after later healthy runs.
|
|
508
|
+
|
|
509
|
+
Unipile currently places acceptance proof at `specifics.shared_secret` and
|
|
510
|
+
inviter data at
|
|
511
|
+
`inviter.{inviter_name,inviter_public_identifier,inviter_description}`.
|
|
512
|
+
SignalDash also reads the legacy top-level secret and old inviter fields. It
|
|
513
|
+
never returns or logs the secret. An invitation missing its ID, acceptance
|
|
514
|
+
proof, or an exact inviter identifier is not accepted: the server emits a
|
|
515
|
+
high-visibility error, stores only sanitized missing-field evidence, and
|
|
516
|
+
exposes it through `sd_auto_accept_status`. Three consecutive parse or provider
|
|
517
|
+
errors disable automation with `disabled_reason:"repeated_errors"`.
|
|
518
|
+
|
|
519
|
+
Use `sd_auto_accept_status` after changing the setting and whenever the user
|
|
520
|
+
asks whether it is healthy. Report `enabled`, exact filters, accepted today and
|
|
521
|
+
this week, failed attempts today, remaining dedicated and shared capacity,
|
|
522
|
+
`consecutive_errors`, `disabled_reason`, and every recent unparseable record.
|
|
523
|
+
Never describe a disabled or erroring worker as active.
|
|
524
|
+
|
|
525
|
+
### Campaign time boundary
|
|
526
|
+
|
|
527
|
+
SignalDash does not expose a general message scheduler. The first executable
|
|
528
|
+
campaign scope has no user-selected future start date, recurring schedule,
|
|
529
|
+
automatic follow-up, acceptance-triggered message, or multi-message
|
|
530
|
+
("double text") sequence. Do not claim that any such action was queued.
|
|
531
|
+
|
|
532
|
+
The common server write authority enforces the design-approved time controls
|
|
533
|
+
for every campaign action:
|
|
534
|
+
|
|
535
|
+
- a valid sender IANA timezone is mandatory;
|
|
536
|
+
- actions run only Monday through Friday, 09:00-17:00 sender-local time;
|
|
537
|
+
- every attempted action atomically stores the next per-sender pacing time,
|
|
538
|
+
randomized from 90 to 180 seconds;
|
|
539
|
+
- timing is checked before final provider preflight and again in the same
|
|
540
|
+
SQLite transaction that reserves budget and acquires the sender lease;
|
|
541
|
+
- one provider write may be in flight per sender;
|
|
542
|
+
- downtime, a work-window boundary, or a UTC reset never creates a catch-up
|
|
543
|
+
burst.
|
|
544
|
+
|
|
545
|
+
Manual one-object tools keep their existing exact-read, approval, duplicate,
|
|
546
|
+
rate, ownership, contact-state, and sender-lock guards. A later message after
|
|
547
|
+
an invitation or reply is new context and requires a fresh thread read, exact
|
|
548
|
+
draft, and human approval.
|
|
549
|
+
|
|
550
|
+
### Human-approved invitation batches
|
|
551
|
+
|
|
552
|
+
An invitation batch is the only executable campaign form. It contains 1–10
|
|
553
|
+
exact canonical LinkedIn Classic profile URLs, one exact inclusion reason per
|
|
554
|
+
target, and one optional exact invitation note per target. The note limit is
|
|
555
|
+
200 Unicode code points. Source labels, reasons, and notes reject leading or
|
|
556
|
+
trailing whitespace; notes normalize line endings and reject control or format
|
|
557
|
+
characters.
|
|
558
|
+
|
|
559
|
+
The human can describe the desired batch naturally, but the agent must
|
|
560
|
+
structure only facts the human explicitly supplied. Never infer or generate a
|
|
561
|
+
profile URL, inclusion reason, or note. Search, discovery, reactions, comments,
|
|
562
|
+
and connection exports are not batch target sources. The server performs no
|
|
563
|
+
runtime target or text generation.
|
|
564
|
+
|
|
565
|
+
Use the batch tools in this order:
|
|
566
|
+
|
|
567
|
+
1. Call `li_create_invitation_batch` once with the exact source label, sender
|
|
568
|
+
IANA timezone, and complete ordered target list.
|
|
569
|
+
2. Poll only with `li_get_invitation_batch` until the durable preview is
|
|
570
|
+
`previewed` or terminal. Inspect every target, including exclusions.
|
|
571
|
+
3. Relay the returned `approval_url` to the human. The human opens it in a
|
|
572
|
+
browser, reauthenticates with the SignalDash invite credential, checks each
|
|
573
|
+
desired target (none are preselected), acknowledges the consequences, and
|
|
574
|
+
approves. MCP and agent bearer tokens cannot approve. Five failed credential
|
|
575
|
+
attempts durably block that batch's authentication surface for 15 minutes.
|
|
576
|
+
4. Continue read-only status checks with `li_get_invitation_batch`. The worker
|
|
577
|
+
submits selected targets in fixed order only while the 24-hour approval,
|
|
578
|
+
sender binding, weekday work window, pacing, budgets, exact identity,
|
|
579
|
+
contact state, invitation state, chat absence, and tenant breaker remain
|
|
580
|
+
valid.
|
|
581
|
+
5. To stop, first inspect the batch, show the exact state and hash, obtain
|
|
582
|
+
cancellation approval, then call `li_cancel_invitation_batch` once with
|
|
583
|
+
that hash and `confirm:true`.
|
|
584
|
+
|
|
585
|
+
Cancel permanently terminates the batch and affects only unstarted targets.
|
|
586
|
+
It cannot recall an executing provider action. A restart requires a new batch,
|
|
587
|
+
preview, and human approval. There is no pause/resume, priority, future start,
|
|
588
|
+
recurring schedule, automatic follow-up, acceptance polling, acceptance
|
|
589
|
+
message, multi-message sequence, or action after a successful invitation.
|
|
590
|
+
|
|
591
|
+
Unknown outcomes, provider warnings, checkpoints, HTTP 403, and HTTP 429 stop
|
|
592
|
+
the batch and lock the sender. Two campaign `outcome_unknown` writes on
|
|
593
|
+
different senders within five minutes open the persisted tenant breaker.
|
|
594
|
+
Tenant provider authentication failures open it immediately. While open, the
|
|
595
|
+
breaker rejects new batch creation and stops queued preview work before any
|
|
596
|
+
provider read.
|
|
597
|
+
Definite provider rejections consume attempts but can leave later exact targets
|
|
598
|
+
eligible to run. SignalDash limits are not LinkedIn-safe thresholds, and
|
|
599
|
+
native LinkedIn activity can still race the final reads.
|
|
600
|
+
|
|
601
|
+
### Search your stored LinkedIn network
|
|
602
|
+
|
|
603
|
+
`li_search_connections` is the primary list-building path before any external
|
|
604
|
+
discovery. It is account-scoped and searches the stored connection sidecar
|
|
605
|
+
only. The request path performs zero LinkedIn, Unipile, HarvestAPI, or other
|
|
606
|
+
paid API calls and never starts or resumes a sync.
|
|
607
|
+
|
|
608
|
+
The required `query` matches accent-insensitive terms across name, headline,
|
|
609
|
+
and stored company. Optional `company` and `headline_keyword` filters are
|
|
610
|
+
case-insensitive substrings. `connected_after` and `connected_before` use
|
|
611
|
+
inclusive `YYYY-MM-DD` bounds. Legacy v1 relation rows do not contain a
|
|
612
|
+
separate company field, so their company filter is transparently matched
|
|
613
|
+
against the returned headline.
|
|
614
|
+
|
|
615
|
+
Each result contains only:
|
|
616
|
+
|
|
617
|
+
- `name`, `headline`, `public_id`, `profile_url`, and `connected_at`;
|
|
618
|
+
- `already_in_contact`, based on exact recorded inbound, outbound, reply, or
|
|
619
|
+
invitation history;
|
|
620
|
+
- the joined `contact_state`, `campaign_eligible`, and `blocking_reasons`.
|
|
621
|
+
|
|
622
|
+
The join checks only the row's exact LinkedIn public identifier and canonical
|
|
623
|
+
profile URL for this SignalDash user. It does not infer a person, merge aliases,
|
|
624
|
+
or inspect another tenant. Search results are planning evidence, not permission
|
|
625
|
+
to message or invite anyone. A later action still requires its exact
|
|
626
|
+
one-object read/preview, human review, approval, fresh preflight, and server
|
|
627
|
+
guards. The executable invitation-batch design does not accept a search result
|
|
628
|
+
as a target input.
|
|
629
|
+
|
|
630
|
+
When no connection rows are stored, the tool returns
|
|
631
|
+
`connections_not_synced` or `connections_sync_pending` and points to:
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
npx -y @floomhq/signaldash connections linkedin-connections.csv
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Use that paced, resumable sync. Never replace the missing snapshot with a burst
|
|
638
|
+
of profile reads.
|
|
639
|
+
|
|
640
|
+
### Capped discovery beyond the stored network
|
|
641
|
+
|
|
642
|
+
`li_discover_people` is the only supported paid discovery path. It searches
|
|
643
|
+
public LinkedIn profile cards through HarvestAPI without using the connected
|
|
644
|
+
LinkedIn sender. It does not fetch full profiles, find email addresses, send
|
|
645
|
+
anything, or create invitation-batch targets.
|
|
646
|
+
|
|
647
|
+
The server enforces local-first behavior. A completed paced connection snapshot
|
|
648
|
+
is mandatory. SignalDash searches that snapshot with the requested role/title
|
|
649
|
+
and company before any paid request. When local matches exist, it returns those
|
|
650
|
+
matches with `source:"own_network"` and `provider_requests:0`; use
|
|
651
|
+
`li_search_connections` to refine them. An incomplete snapshot returns
|
|
652
|
+
`connections_sync_required` and starts no provider request.
|
|
653
|
+
|
|
654
|
+
When there are no local matches:
|
|
655
|
+
|
|
656
|
+
1. Call `li_discover_people` without `confirm`. The response previews the exact
|
|
657
|
+
query, filters, result limit, ten-profile maximum billing exposure, maximum
|
|
658
|
+
reserved cost, and 30-minute expiry. It makes zero paid requests.
|
|
659
|
+
2. Show the human that exact paid request and maximum reserved cost. Obtain
|
|
660
|
+
explicit approval.
|
|
661
|
+
3. Repeat the identical arguments once with `confirm:true`.
|
|
662
|
+
|
|
663
|
+
The hosted server then makes at most one serialized
|
|
664
|
+
`GET /linkedin/profile-search` request for page 1. It reserves the configured
|
|
665
|
+
worst-case page cost in integer micro-dollars before the request and enforces a
|
|
666
|
+
durable per-user cooldown, per-user daily request cap, tenant-wide daily
|
|
667
|
+
request cap, and tenant-wide daily cost cap. The reservation consumes the exact
|
|
668
|
+
preview atomically. Another paid request, including one after a failed or
|
|
669
|
+
ambiguous provider result, requires a fresh preview and approval. Failed or
|
|
670
|
+
ambiguous provider requests retain their reservation and are never retried
|
|
671
|
+
automatically.
|
|
672
|
+
|
|
673
|
+
SignalDash removes hidden `"LinkedIn Member"` cards, malformed or ambiguous
|
|
674
|
+
identities, duplicates, exact stored connections, and exact contact-state rows
|
|
675
|
+
already marked connected. Returned cards contain only name, headline,
|
|
676
|
+
location, public identifier, canonical profile URL, and exact local contact
|
|
677
|
+
state. No fuzzy or cross-channel identity merge occurs.
|
|
678
|
+
|
|
679
|
+
Discovery results are planning evidence only. `CAMPAIGN-DESIGN.md` excludes
|
|
680
|
+
searches and discovery as invitation-batch target sources. A later action on
|
|
681
|
+
one exact person begins the complete one-object preview, approval, fresh
|
|
682
|
+
preflight, contact-state, duplicate, ownership, and rate-limit flow.
|
|
683
|
+
|
|
684
|
+
### LinkedIn connections export via CLI
|
|
685
|
+
|
|
686
|
+
This is intentionally a paced CLI workflow, not an MCP bulk-read tool.
|
|
687
|
+
|
|
688
|
+
```bash
|
|
689
|
+
npx -y @floomhq/signaldash connections linkedin-connections.csv
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
Use it when the user asks to export or download their LinkedIn connections.
|
|
693
|
+
The server retrieves one page at a time in a resumable background sync. Do not
|
|
694
|
+
replace it with per-profile reads or parallel requests.
|
|
695
|
+
|
|
696
|
+
Progress and success resemble:
|
|
697
|
+
|
|
698
|
+
```text
|
|
699
|
+
Syncing connections (paced to keep your account safe)...
|
|
700
|
+
Synced 300 connections (3 pages)...
|
|
701
|
+
842 connections
|
|
702
|
+
+ saved linkedin-connections.csv
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
If the sync pauses, the CLI can save the rows fetched so far and prints that
|
|
706
|
+
the sync is still running. Running the same command later resumes it. If it
|
|
707
|
+
prints `nothing fetched yet, run again later to resume`, no CSV was produced
|
|
708
|
+
and the command exits with failure.
|
|
709
|
+
|
|
710
|
+
## Server-enforced guards: these will reject you
|
|
711
|
+
|
|
712
|
+
These controls run on the server. Prompt instructions, a new session, or a
|
|
713
|
+
different calling order cannot bypass them.
|
|
714
|
+
|
|
715
|
+
### 428 `read_before_send_required`
|
|
716
|
+
|
|
717
|
+
Meaning: this user has not successfully read the exact chat recently, or the
|
|
718
|
+
email recipient was not present in a recently read thread, or an invitation
|
|
719
|
+
write lacks its exact recent preview/list read. The default read window is 30
|
|
720
|
+
minutes.
|
|
721
|
+
|
|
722
|
+
Comply:
|
|
723
|
+
|
|
724
|
+
1. Call the matching read tool on the exact chat or email thread.
|
|
725
|
+
2. Inspect at least the last 10 items for identity, context, and duplicates.
|
|
726
|
+
3. If the context changed, revise the draft and obtain approval again.
|
|
727
|
+
4. Send once while the read is recent.
|
|
728
|
+
|
|
729
|
+
Do not satisfy this guard by reading a different chat with a similar name.
|
|
730
|
+
|
|
731
|
+
### 409 `duplicate_send`
|
|
732
|
+
|
|
733
|
+
Meaning: the exact message was already sent to that chat, or the exact email
|
|
734
|
+
recipient, subject, and body combination was already sent.
|
|
735
|
+
|
|
736
|
+
Comply:
|
|
737
|
+
|
|
738
|
+
1. Do not retry.
|
|
739
|
+
2. Re-read the exact thread or sent history.
|
|
740
|
+
3. Tell the user when the matching outbound item was sent, using `sent_at` when
|
|
741
|
+
returned.
|
|
742
|
+
4. Draft different text only when the user has a genuine follow-up intent, then
|
|
743
|
+
obtain new approval.
|
|
744
|
+
|
|
745
|
+
Changing whitespace or punctuation to evade the duplicate guard is prohibited.
|
|
746
|
+
|
|
747
|
+
### 409 `contact_suppressed`
|
|
748
|
+
|
|
749
|
+
Meaning: one of the exact channel identifiers for this contact has an active
|
|
750
|
+
suppression. Message and invitation writes stop before the provider write.
|
|
751
|
+
|
|
752
|
+
Comply:
|
|
753
|
+
|
|
754
|
+
1. Stop the action.
|
|
755
|
+
2. Inspect the exact state with `sd_contact_state`.
|
|
756
|
+
3. Do not switch identifiers, channels, accounts, or sessions to evade it.
|
|
757
|
+
4. Escalate a mistaken suppression to the SignalDash operator. MCP cannot clear
|
|
758
|
+
it.
|
|
759
|
+
|
|
760
|
+
Campaign-mode actions also reject `recipient_replied` and `already_contacted`.
|
|
761
|
+
These blocks prevent automated follow-up after a known inbound message and
|
|
762
|
+
prevent another campaign from touching the same exact identifier. They do not
|
|
763
|
+
claim that SignalDash has resolved a person across channels.
|
|
764
|
+
|
|
765
|
+
### 409 invitation and context preflight blocks
|
|
766
|
+
|
|
767
|
+
`thread_changed`, `already_connected`, `existing_conversation`,
|
|
768
|
+
`conversation_state_incomplete`, `invitation_already_pending`,
|
|
769
|
+
`inbound_invitation_pending`, `invitation_not_pending`,
|
|
770
|
+
`invitation_state_incomplete`, and `relationship_unverified` mean the exact
|
|
771
|
+
provider state no longer authorizes the action. New-invitation preflight checks
|
|
772
|
+
up to 250 sent invitations, up to 100 received invitations, and the exact
|
|
773
|
+
target's attendee-scoped chats. Any pagination cursor makes absence unproved.
|
|
774
|
+
|
|
775
|
+
Comply:
|
|
776
|
+
|
|
777
|
+
1. Stop the action.
|
|
778
|
+
2. Re-read the exact thread or invitation list.
|
|
779
|
+
3. Do not expand pagination or fetch profiles in a loop.
|
|
780
|
+
4. Obtain new approval only for a newly previewed exact action.
|
|
781
|
+
|
|
782
|
+
### 429 `rate_limit_exceeded`
|
|
783
|
+
|
|
784
|
+
Meaning: the persisted daily action cap is exhausted. The response includes
|
|
785
|
+
`rate_limit.limit`, `used`, `remaining`, and `resets_at`, plus `Retry-After` and
|
|
786
|
+
matching rate-limit headers.
|
|
787
|
+
|
|
788
|
+
Comply:
|
|
789
|
+
|
|
790
|
+
1. Stop all sends on that account.
|
|
791
|
+
2. Report the limit and reset time.
|
|
792
|
+
3. Wait until the reset. Do not switch tokens, sessions, channels, or machines
|
|
793
|
+
to work around the cap.
|
|
794
|
+
4. Do not queue a burst for the reset boundary.
|
|
795
|
+
|
|
796
|
+
Invitation sends also return `invitation_rate_limit_exceeded` when the separate
|
|
797
|
+
weekly policy is exhausted. The default policy is 100 attempts per sender,
|
|
798
|
+
resetting Monday at 00:00 UTC. It is a SignalDash policy, not a claim about a
|
|
799
|
+
LinkedIn-safe threshold.
|
|
800
|
+
|
|
801
|
+
Paid discovery returns `paid_discovery_cooldown`,
|
|
802
|
+
`paid_discovery_user_daily_cap`, `paid_discovery_tenant_daily_cap`, or
|
|
803
|
+
`paid_discovery_cost_cap` before the HarvestAPI request when its pacing,
|
|
804
|
+
request, or spend boundary is reached. Stop and use `retry_at` or the UTC reset
|
|
805
|
+
reported by the server. Do not switch users, sessions, or machines to bypass a
|
|
806
|
+
paid-provider cap.
|
|
807
|
+
|
|
808
|
+
### 423 sender locks and 502 `outcome_unknown`
|
|
809
|
+
|
|
810
|
+
A provider warning, HTTP 403, HTTP 429, sender identity mismatch, or ambiguous
|
|
811
|
+
provider outcome locks all later LinkedIn writes for that logical sender. An
|
|
812
|
+
ambiguous result returns `outcome_unknown`, consumes the action budget, and is
|
|
813
|
+
never retryable.
|
|
814
|
+
|
|
815
|
+
Comply:
|
|
816
|
+
|
|
817
|
+
1. Do not retry or switch sessions.
|
|
818
|
+
2. Inspect the provider account manually for a restriction and verify whether
|
|
819
|
+
the action landed.
|
|
820
|
+
3. Escalate for human reconciliation. Only the hosted operator can clear the
|
|
821
|
+
durable lock after verification.
|
|
822
|
+
|
|
823
|
+
Any upstream 429, provider warning, checkpoint, restriction, unusual-activity
|
|
824
|
+
prompt, or HTTP 403 also means stop. Do not retry.
|
|
825
|
+
|
|
826
|
+
## Before every send
|
|
827
|
+
|
|
828
|
+
These rules apply to LinkedIn messages and invitations, WhatsApp, email, and
|
|
829
|
+
public LinkedIn posts.
|
|
830
|
+
|
|
831
|
+
1. Never send or publish without explicit human approval of the exact
|
|
832
|
+
recipient or audience and the exact final text. The only standing-action
|
|
833
|
+
exception is invitation auto-accept after explicit approval of its exact
|
|
834
|
+
persistent setting and filters.
|
|
835
|
+
2. Read the exact thread immediately before a message action. For invitations,
|
|
836
|
+
preview the exact target and note or list the exact current invitation.
|
|
837
|
+
Check the recipient, latest inbound message, prior context, relationship,
|
|
838
|
+
pending state, and whether the proposed action already exists.
|
|
839
|
+
3. Never infer a recipient from a partial name. Resolve duplicate names with
|
|
840
|
+
the user.
|
|
841
|
+
4. Never loop or parallelize single-object send tools. Only the immutable
|
|
842
|
+
invitation-batch runner may submit more than one target, and only after the
|
|
843
|
+
separate exact browser approval.
|
|
844
|
+
5. Never turn reactions, comments, connections, or exported rows into an
|
|
845
|
+
unsolicited outreach list.
|
|
846
|
+
6. Send one message at human pace. Let server pacing finish.
|
|
847
|
+
7. Stop on a provider warning, checkpoint, restriction, authentication
|
|
848
|
+
anomaly, delivery anomaly, HTTP 403, or HTTP 429.
|
|
849
|
+
8. Never retry an ambiguous timeout before re-reading the exact thread or sent
|
|
850
|
+
history. A timeout can hide a successful send.
|
|
851
|
+
9. Confirm delivery by reading the thread after the action.
|
|
852
|
+
10. Account health outranks throughput and task completion.
|
|
853
|
+
|
|
854
|
+
The hosted backend defaults to 20 LinkedIn action attempts per stable logical
|
|
855
|
+
sender per UTC day, shared across messages, invitations, invitation
|
|
856
|
+
maintenance, and post publishing. Invitation sends also use the default
|
|
857
|
+
100-attempt weekly policy. Email and WhatsApp retain their hosted send caps. A
|
|
858
|
+
deployment can configure different caps. Never promise a particular remaining
|
|
859
|
+
allowance until the response reports `rate_limit.limit` and `remaining`.
|
|
860
|
+
|
|
861
|
+
## Worked flows
|
|
862
|
+
|
|
863
|
+
### Flow 1: triage unread LinkedIn chats and draft replies
|
|
864
|
+
|
|
865
|
+
User: "Check my unread LinkedIn chats and draft replies."
|
|
866
|
+
|
|
867
|
+
1. Confirm LinkedIn is connected with `signaldash status` when unknown.
|
|
868
|
+
2. Call:
|
|
869
|
+
|
|
870
|
+
```text
|
|
871
|
+
li_list_chats({"limit":20})
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
3. Select only items with `unread_count > 0`. Do not claim that the list tool
|
|
875
|
+
alone contains full conversation context.
|
|
876
|
+
4. Read each selected conversation one at a time:
|
|
877
|
+
|
|
878
|
+
```text
|
|
879
|
+
li_read_messages({"chat_id":"chat_li_7f3a","limit":20})
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
5. Return a compact triage:
|
|
883
|
+
|
|
884
|
+
```text
|
|
885
|
+
Amina Rahman, 2 unread
|
|
886
|
+
Last message: asks for the case study.
|
|
887
|
+
Suggested reply: "Yes. I’ll send it over this afternoon."
|
|
888
|
+
|
|
889
|
+
Marco Silva, 1 unread
|
|
890
|
+
Last message: confirms Tuesday at 10:00.
|
|
891
|
+
Suggested reply: "Tuesday at 10:00 works. See you then."
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
6. Stop. Drafting is not sending. Ask for approval of each exact reply.
|
|
895
|
+
7. For any approved reply, re-read that exact chat, send once, and read again
|
|
896
|
+
to verify. Do not send all drafts in a loop.
|
|
897
|
+
|
|
898
|
+
### Flow 2: find who engaged with the last post and recommend action
|
|
899
|
+
|
|
900
|
+
User: "Who engaged with my last LinkedIn post, and what should I do?"
|
|
901
|
+
|
|
902
|
+
1. Call:
|
|
903
|
+
|
|
904
|
+
```text
|
|
905
|
+
li_my_posts({"limit":5})
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
2. Select the newest post by its provider timestamp, not by list position when
|
|
909
|
+
timestamps disagree. Record its exact `post_id`.
|
|
910
|
+
3. Call:
|
|
911
|
+
|
|
912
|
+
```text
|
|
913
|
+
li_post_reactions({"post_id":"post_urn_8821","limit":50})
|
|
914
|
+
li_post_comments({"post_id":"post_urn_8821","limit":50})
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
4. Group the result:
|
|
918
|
+
|
|
919
|
+
- Direct questions in comments: respond publicly or draft a reply.
|
|
920
|
+
- Existing contacts with substantive engagement: warm follow-up candidate,
|
|
921
|
+
but no message without thread verification and approval.
|
|
922
|
+
- Reactions without context: signal only, no automatic outreach.
|
|
923
|
+
- Spam or irrelevant engagement: no action.
|
|
924
|
+
|
|
925
|
+
5. Return names, the evidence for prioritization, and proposed next actions.
|
|
926
|
+
Do not invent profile facts absent from the result.
|
|
927
|
+
6. If the user asks to message one person, locate the exact existing chat,
|
|
928
|
+
read it, draft a contextual message, obtain exact approval, re-read, and
|
|
929
|
+
send once. If the user explicitly asks to invite one exact reactor, resolve
|
|
930
|
+
the exact provider ID, run the invitation preview, obtain exact approval,
|
|
931
|
+
and send once. Engagement never authorizes an invitation or a loop.
|
|
932
|
+
|
|
933
|
+
### Flow 3: find and reply to a WhatsApp thread
|
|
934
|
+
|
|
935
|
+
User: "Find my WhatsApp thread with Sara and reply that 16:30 works."
|
|
936
|
+
|
|
937
|
+
1. Call:
|
|
938
|
+
|
|
939
|
+
```text
|
|
940
|
+
wa_list_chats({"limit":20})
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
2. Resolve the exact Sara. If multiple chats or group/direct variants match,
|
|
944
|
+
show the candidates and ask which one.
|
|
945
|
+
3. Read:
|
|
946
|
+
|
|
947
|
+
```text
|
|
948
|
+
wa_read_messages({"chat_id":"chat_wa_91b2","limit":20})
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
4. Confirm that the latest context is about scheduling and draft:
|
|
952
|
+
|
|
953
|
+
```text
|
|
954
|
+
Recipient: Sara Conti, WhatsApp
|
|
955
|
+
Text: "16:30 works for me. See you then."
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
5. Ask for exact approval unless the user's original request already approved
|
|
959
|
+
that exact recipient and exact final text. Paraphrasing the user's words
|
|
960
|
+
creates a new draft that needs approval.
|
|
961
|
+
6. Immediately re-read `chat_wa_91b2`.
|
|
962
|
+
7. Send once:
|
|
963
|
+
|
|
964
|
+
```text
|
|
965
|
+
wa_send_message({
|
|
966
|
+
"chat_id":"chat_wa_91b2",
|
|
967
|
+
"text":"16:30 works for me. See you then."
|
|
968
|
+
})
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
8. Read the chat again and confirm the text appears once as an outbound
|
|
972
|
+
message.
|
|
973
|
+
|
|
974
|
+
### Flow 4: send, accept, or withdraw one LinkedIn invitation
|
|
975
|
+
|
|
976
|
+
For a new invitation:
|
|
977
|
+
|
|
978
|
+
1. Resolve one exact `provider_id`. A display name or fuzzy match is
|
|
979
|
+
insufficient.
|
|
980
|
+
2. Preview without confirmation:
|
|
981
|
+
|
|
982
|
+
```text
|
|
983
|
+
li_send_invitation({
|
|
984
|
+
"provider_id":"ACoAAExactMember",
|
|
985
|
+
"note":"Hi Amina, I enjoyed your post on agent safety."
|
|
986
|
+
})
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
3. Show the server-confirmed sender, target, exact note, and character count.
|
|
990
|
+
4. Obtain approval for that exact target and note.
|
|
991
|
+
5. Repeat the identical payload once with `"confirm":true`.
|
|
992
|
+
6. Stop on relationship, existing-conversation, incomplete-state,
|
|
993
|
+
pending-invitation, cap, warning, lock, or `outcome_unknown` errors. Never
|
|
994
|
+
retry an ambiguous result.
|
|
995
|
+
|
|
996
|
+
For received or sent invitation maintenance:
|
|
997
|
+
|
|
998
|
+
1. Call `li_invitations_received` or `li_invitations_sent`.
|
|
999
|
+
2. Resolve the exact invitation ID and show the exact person and action.
|
|
1000
|
+
3. Obtain approval.
|
|
1001
|
+
4. Call `li_accept_invitation` or `li_withdraw_invitation` once with that ID
|
|
1002
|
+
and `"confirm":true`.
|
|
1003
|
+
5. List again to verify that the invitation is no longer pending.
|
|
1004
|
+
|
|
1005
|
+
Never loop over the returned page. Incoming acceptance and stale withdrawal
|
|
1006
|
+
are human-approved, single-object operations only.
|
|
1007
|
+
|
|
1008
|
+
### Flow 4b: enable and inspect invitation auto-accept
|
|
1009
|
+
|
|
1010
|
+
User: "Auto-accept LinkedIn invitations from these exact public IDs when their
|
|
1011
|
+
description contains Founder."
|
|
1012
|
+
|
|
1013
|
+
1. Call `sd_settings_get({})`, then show the exact persistent setting:
|
|
1014
|
+
|
|
1015
|
+
```text
|
|
1016
|
+
Enabled: true
|
|
1017
|
+
Public identifiers: amina-rahman, marco-silva
|
|
1018
|
+
Description keywords: Founder
|
|
1019
|
+
Matching rule: exact public ID AND description keyword
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
2. Obtain explicit approval, then call once:
|
|
1023
|
+
|
|
1024
|
+
```text
|
|
1025
|
+
sd_settings_set({
|
|
1026
|
+
"auto_accept_linkedin":true,
|
|
1027
|
+
"auto_accept_linkedin_filters":{
|
|
1028
|
+
"public_identifiers":["amina-rahman","marco-silva"],
|
|
1029
|
+
"description_keywords":["Founder"]
|
|
1030
|
+
},
|
|
1031
|
+
"confirm":true
|
|
1032
|
+
})
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
3. Verify with:
|
|
1036
|
+
|
|
1037
|
+
```text
|
|
1038
|
+
sd_auto_accept_status({})
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
4. Report the setting, accepted count today and this week, failed attempts
|
|
1042
|
+
today, both remaining capacities, repeated-error state, and every
|
|
1043
|
+
unparseable invitation. If the worker disabled itself, stop and surface the
|
|
1044
|
+
exact `disabled_reason` and missing fields. Do not silently re-enable it.
|
|
1045
|
+
5. To disable, obtain explicit approval and call
|
|
1046
|
+
`sd_settings_set({"auto_accept_linkedin":false,"confirm":true})`, then
|
|
1047
|
+
verify both `sd_settings_get({})` and status.
|
|
1048
|
+
|
|
1049
|
+
### Flow 5: search LinkedIn connections locally
|
|
1050
|
+
|
|
1051
|
+
User: "Find product leaders at Acme in my LinkedIn network."
|
|
1052
|
+
|
|
1053
|
+
1. Call:
|
|
1054
|
+
|
|
1055
|
+
```text
|
|
1056
|
+
li_search_connections({
|
|
1057
|
+
"query":"product leader",
|
|
1058
|
+
"filters":{"company":"Acme"},
|
|
1059
|
+
"limit":20
|
|
1060
|
+
})
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
2. Confirm the response has `local_only:true` and `provider_requests:0`.
|
|
1064
|
+
3. Return only the stored profile fields and exact contact-state result. Do not
|
|
1065
|
+
add inferred profile facts or cross-channel identity.
|
|
1066
|
+
4. If the response says `connections_not_synced` or
|
|
1067
|
+
`connections_sync_pending`, relay its paced-sync instruction. Do not trigger
|
|
1068
|
+
profile reads or discovery as a substitute.
|
|
1069
|
+
5. Stop after the read-only results. A later action on one exact person starts
|
|
1070
|
+
the complete guarded one-object flow with fresh provider context and
|
|
1071
|
+
approval. Never turn the result page into a send loop.
|
|
1072
|
+
|
|
1073
|
+
### Flow 6: export LinkedIn connections
|
|
1074
|
+
|
|
1075
|
+
User: "Export my LinkedIn connections to CSV."
|
|
1076
|
+
|
|
1077
|
+
1. Confirm LinkedIn is connected.
|
|
1078
|
+
2. Choose a user-visible output path. Never overwrite an existing file without
|
|
1079
|
+
explicit authorization.
|
|
1080
|
+
3. Run:
|
|
1081
|
+
|
|
1082
|
+
```bash
|
|
1083
|
+
npx -y @floomhq/signaldash connections linkedin-connections.csv
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
4. Let the paced sync run. Do not start another export in parallel.
|
|
1087
|
+
5. Success is the `+ saved <path>` line and a non-empty CSV with this header:
|
|
1088
|
+
|
|
1089
|
+
```text
|
|
1090
|
+
name,headline,public_id,profile_url,connected_at
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
6. Report the saved path and row count. If the CLI reports a paused or partial
|
|
1094
|
+
sync, say that the export is partial and rerun later to resume.
|
|
1095
|
+
|
|
1096
|
+
### Flow 7: discover people beyond the stored network
|
|
1097
|
+
|
|
1098
|
+
User: "Find platform engineers at Acme in Berlin beyond my network."
|
|
1099
|
+
|
|
1100
|
+
1. Call without confirmation:
|
|
1101
|
+
|
|
1102
|
+
```text
|
|
1103
|
+
li_discover_people({
|
|
1104
|
+
"query":"Platform Engineer",
|
|
1105
|
+
"filters":{"company":"Acme","location":"Berlin"},
|
|
1106
|
+
"limit":5
|
|
1107
|
+
})
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
2. If it returns `source:"own_network"`, return those local matches and stop.
|
|
1111
|
+
The paid provider was not called.
|
|
1112
|
+
3. If it returns `connections_sync_required`, run or resume the paced
|
|
1113
|
+
connections sync and stop. Do not bypass it.
|
|
1114
|
+
4. If it returns a paid preview, show the exact query, filters, result limit,
|
|
1115
|
+
and `maximum_cost_reserved_usd`. Obtain explicit approval.
|
|
1116
|
+
5. Repeat the identical payload once with `"confirm":true`.
|
|
1117
|
+
6. Report only the returned public profile-card fields and exact contact state.
|
|
1118
|
+
State that hidden/unusable and exact already-connected cards were excluded.
|
|
1119
|
+
7. Stop after the read-only results. Do not enrich profiles, find emails,
|
|
1120
|
+
create a batch, or loop into invitations or messages.
|
|
1121
|
+
|
|
1122
|
+
### Flow 8: create, review, or cancel one invitation batch
|
|
1123
|
+
|
|
1124
|
+
User: "Invite these two exact LinkedIn profiles with these notes."
|
|
1125
|
+
|
|
1126
|
+
1. Confirm the human supplied every canonical profile URL, exact inclusion
|
|
1127
|
+
reason, exact optional note, source label, and sender timezone. Ask for any
|
|
1128
|
+
missing value. Do not fill gaps with search or generation.
|
|
1129
|
+
2. Call:
|
|
1130
|
+
|
|
1131
|
+
```text
|
|
1132
|
+
li_create_invitation_batch({
|
|
1133
|
+
"source_label":"User-approved event follow-up",
|
|
1134
|
+
"time_zone":"Europe/Berlin",
|
|
1135
|
+
"targets":[
|
|
1136
|
+
{
|
|
1137
|
+
"profile_url":"https://www.linkedin.com/in/amina-rahman/",
|
|
1138
|
+
"inclusion_reason":"The user met Amina at the named event",
|
|
1139
|
+
"note":"Hi Amina, great meeting you at the agent safety meetup."
|
|
1140
|
+
}
|
|
1141
|
+
]
|
|
1142
|
+
})
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
3. Store the exact `batch_id`. Call `li_get_invitation_batch` until its state
|
|
1146
|
+
is `previewed` or terminal. Report every excluded row and reason.
|
|
1147
|
+
4. Show the sender, source, ordered targets, full profile URLs, reasons, exact
|
|
1148
|
+
notes, reused-copy counts, current limits, timing, expiry, cancellation
|
|
1149
|
+
limit, and residual native-activity race.
|
|
1150
|
+
5. Relay the exact `approval_url`. Do not fetch, submit, or automate that
|
|
1151
|
+
browser page. The human reauthenticates and selects the desired unchecked
|
|
1152
|
+
rows.
|
|
1153
|
+
6. Inspect the batch to report `approved`, `running`, or terminal progress.
|
|
1154
|
+
Never interpret an approval-page visit as approval; only server state proves
|
|
1155
|
+
it.
|
|
1156
|
+
7. For cancellation, inspect immediately, show the exact state and
|
|
1157
|
+
`approval_view_hash`, obtain explicit cancellation approval, then call:
|
|
1158
|
+
|
|
1159
|
+
```text
|
|
1160
|
+
li_cancel_invitation_batch({
|
|
1161
|
+
"batch_id":"00000000-0000-4000-8000-000000000000",
|
|
1162
|
+
"approval_view_hash":"the exact hash from the fresh read",
|
|
1163
|
+
"confirm":true
|
|
1164
|
+
})
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
8. Report that planned targets were cancelled and any executing action was not
|
|
1168
|
+
recalled. Never claim the batch was paused or can resume.
|
|
1169
|
+
|
|
1170
|
+
## Read-only requests and drafts
|
|
1171
|
+
|
|
1172
|
+
Read-only account work does not require send approval. You can list, read,
|
|
1173
|
+
search the stored connection snapshot, summarize, analyze engagement, and
|
|
1174
|
+
create a non-published post draft as requested. Paid discovery separately
|
|
1175
|
+
requires approval of its exact preview and maximum reserved cost. Still use
|
|
1176
|
+
narrow limits, protect private content, and avoid bulk collection. Search and
|
|
1177
|
+
discovery results do not authorize an outbound action.
|
|
1178
|
+
|
|
1179
|
+
For every draft, label it clearly as a draft. Never present a suggested reply
|
|
1180
|
+
as sent. Never present `{"drafted":true,"published":false}` as a published post.
|
|
1181
|
+
|
|
1182
|
+
## Logout
|
|
1183
|
+
|
|
1184
|
+
When the user explicitly asks to disconnect this local SignalDash session, run:
|
|
1185
|
+
|
|
1186
|
+
```bash
|
|
1187
|
+
npx -y @floomhq/signaldash logout
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
Success:
|
|
1191
|
+
|
|
1192
|
+
```text
|
|
1193
|
+
Logged out of SignalDash. The session was revoked.
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
Logout revokes the current SignalDash session and removes its local token. It
|
|
1197
|
+
does not authorize deleting provider accounts or messages.
|
|
1198
|
+
|
|
1199
|
+
## Final checklist
|
|
1200
|
+
|
|
1201
|
+
Before reporting completion, verify:
|
|
1202
|
+
|
|
1203
|
+
- The skill is persisted in a discovered skill directory.
|
|
1204
|
+
- `status` proves the required provider is connected.
|
|
1205
|
+
- The exact chat, thread, post, or export path was resolved.
|
|
1206
|
+
- A connection search reported `local_only:true`, made zero provider requests,
|
|
1207
|
+
and did not authorize an outbound action.
|
|
1208
|
+
- A paid discovery had a completed connection snapshot, returned no own-network
|
|
1209
|
+
matches, used an unexpired exact preview and approval, made at most one
|
|
1210
|
+
serialized provider request, reported its cost budget, and remained planning
|
|
1211
|
+
evidence only.
|
|
1212
|
+
- Every send used a fresh exact-thread read and exact human approval.
|
|
1213
|
+
- Every invitation send used an exact server preview and exact approval.
|
|
1214
|
+
- Every invitation accept or withdrawal used a fresh exact invitation-list
|
|
1215
|
+
read and exact approval.
|
|
1216
|
+
- Invitation auto-accept was explicitly enabled for the exact user, preserved
|
|
1217
|
+
both the dedicated and shared caps, stopped on repeated errors, and exposed
|
|
1218
|
+
every unparseable invitation through `sd_auto_accept_status`.
|
|
1219
|
+
- Every invitation batch used only exact user-supplied canonical profile URLs,
|
|
1220
|
+
reasons, and notes; exposed every target; used the separate browser approval;
|
|
1221
|
+
and never approved through MCP.
|
|
1222
|
+
- Every batch cancel followed a fresh exact batch read and stated that an
|
|
1223
|
+
executing action cannot be recalled.
|
|
1224
|
+
- No duplicate, bulk, parallel, warning, 403, or 429 path was bypassed.
|
|
1225
|
+
- No sender lock or `outcome_unknown` result was retried.
|
|
1226
|
+
- A send was confirmed by a post-send read.
|
|
1227
|
+
- A post draft remained unpublished unless `publish:true` was explicitly
|
|
1228
|
+
approved.
|
|
1229
|
+
- An export ended with `+ saved` and a non-empty CSV.
|
|
1230
|
+
|
|
1231
|
+
If any item is unverified, state exactly what remains incomplete.
|
|
1232
|
+
|
|
1233
|
+
## How to write the message (this is where agents fail hardest)
|
|
1234
|
+
|
|
1235
|
+
Agents pad. Padding is the clearest tell that a human did not write it, and on
|
|
1236
|
+
LinkedIn or WhatsApp it gets ignored. Less is more, always.
|
|
1237
|
+
|
|
1238
|
+
- **Short.** A reply is usually 1-3 sentences. If they wrote one line, reply
|
|
1239
|
+
with one line. Match the length and register of the thread.
|
|
1240
|
+
- **One idea per message.** Do not stack context, ask and pleasantry into one
|
|
1241
|
+
block. Split into 2-3 short consecutive sends instead of one paragraph.
|
|
1242
|
+
- **No preamble, no summary-back.** Never "I hope this finds you well", never
|
|
1243
|
+
restate what they just said, never a formal sign-off in a chat.
|
|
1244
|
+
- **No em dashes.** Use commas, periods, colons.
|
|
1245
|
+
- **No hype filler.** Cut "excited to", "reaching out", "just wanted to",
|
|
1246
|
+
"circling back", "leverage", "synergies", and any eager closer.
|
|
1247
|
+
- **Their language.** German thread stays German, with real umlauts (für, not
|
|
1248
|
+
fuer). Never translate their language away.
|
|
1249
|
+
- **Read the thread first, then sound like the user.** Their own recent messages
|
|
1250
|
+
in that thread are the style reference. Copy that register, not a template.
|
|
1251
|
+
|
|
1252
|
+
Before/after, same intent:
|
|
1253
|
+
|
|
1254
|
+
> Bad: "Hi Mircea, I hope you're doing well! Thanks so much for reaching out
|
|
1255
|
+
> about scheduling a call. I'd be delighted to connect and would love to explore
|
|
1256
|
+
> how we might be able to work together. Please let me know what times work best
|
|
1257
|
+
> for you and I'll do my best to accommodate your schedule."
|
|
1258
|
+
|
|
1259
|
+
> Good: "hey Mircea, ja gerne. hier mein link: [cal]"
|
|
1260
|
+
|
|
1261
|
+
If a draft is longer than the thread's own messages, cut it before showing it.
|
|
1262
|
+
|