@floomhq/signaldash 0.11.2 → 0.13.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 +33 -11
- package/bin/sd.mjs +35 -4
- package/package.json +2 -2
- package/skills/signaldash/SKILL.md +626 -91
package/README.md
CHANGED
|
@@ -11,6 +11,14 @@ the server's Unipile access key.
|
|
|
11
11
|
You need Node.js 20 or newer and a single-use SignalDash invite code. Ask the
|
|
12
12
|
SignalDash administrator for an invite code, then run:
|
|
13
13
|
|
|
14
|
+
```bash
|
|
15
|
+
npx -y @floomhq/signaldash <invite-code>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This agent-first setup logs in, installs the bundled operating skill, registers
|
|
19
|
+
the MCP server with Claude Code when available, and prints hosted-auth links for
|
|
20
|
+
the human to complete. The equivalent manual sequence is:
|
|
21
|
+
|
|
14
22
|
```bash
|
|
15
23
|
npx -y @floomhq/signaldash login <invite-code>
|
|
16
24
|
npx -y @floomhq/signaldash connect linkedin
|
|
@@ -74,18 +82,22 @@ The MCP server uses the user token created by `login`. It cannot access a
|
|
|
74
82
|
LinkedIn, WhatsApp, or email account until that channel has been connected for
|
|
75
83
|
the same logged-in user.
|
|
76
84
|
|
|
77
|
-
## Give the agent the
|
|
85
|
+
## Give the agent the operating skill
|
|
86
|
+
|
|
87
|
+
The canonical [`signaldash` skill](skills/signaldash/SKILL.md) teaches an agent
|
|
88
|
+
when to use SignalDash, how to persist the skill, complete setup, interpret
|
|
89
|
+
connection state, operate every tool, handle server guards, and run safe worked
|
|
90
|
+
flows. It is also served as raw Markdown for agents at:
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
history, check for duplicates, and obtain approval when the recipient or text
|
|
83
|
-
has not already been approved.
|
|
92
|
+
```text
|
|
93
|
+
https://signaldash.dev/SKILL.md
|
|
94
|
+
```
|
|
84
95
|
|
|
85
|
-
The
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
The one-command setup installs the bundled copy into Claude Code. Other clients
|
|
97
|
+
must save the skill in their documented skill directory and reload skill
|
|
98
|
+
discovery. The narrower
|
|
99
|
+
[`signaldash-safe-usage` skill](skills/signaldash-safe-usage/SKILL.md) remains
|
|
100
|
+
available for clients that separate operating and send-safety instructions.
|
|
89
101
|
|
|
90
102
|
## MCP tools
|
|
91
103
|
|
|
@@ -100,13 +112,23 @@ SignalDash exposes:
|
|
|
100
112
|
- `email_list(limit)`
|
|
101
113
|
- `email_read(thread_id, limit)`
|
|
102
114
|
- `email_send(to, subject, body)`
|
|
115
|
+
- `li_my_posts(limit, member_id)`
|
|
116
|
+
- `li_post_reactions(post_id, limit)`
|
|
117
|
+
- `li_post_comments(post_id, limit)`
|
|
118
|
+
- `li_draft_post(text, publish)`
|
|
103
119
|
|
|
104
120
|
Every operation runs through the hosted SignalDash backend. Agents never
|
|
105
121
|
receive the Unipile access key.
|
|
106
122
|
|
|
123
|
+
The CLI also provides a paced, resumable LinkedIn connections export:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx -y @floomhq/signaldash connections linkedin-connections.csv
|
|
127
|
+
```
|
|
128
|
+
|
|
107
129
|
## Safety limits
|
|
108
130
|
|
|
109
|
-
SignalDash enforces a daily send cap. The
|
|
131
|
+
SignalDash enforces a daily send cap. The operating skill adds the human
|
|
110
132
|
workflow around that runtime control:
|
|
111
133
|
|
|
112
134
|
- Every successful send includes `rate_limit.limit`, `used`, `remaining`, and
|
package/bin/sd.mjs
CHANGED
|
@@ -22,6 +22,9 @@ async function ui() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const DEFAULT_BACKEND = process.env.SIGNALDASH_BACKEND || "https://signaldash-api.floom.dev";
|
|
25
|
+
const PACKAGE_VERSION = JSON.parse(
|
|
26
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
27
|
+
).version;
|
|
25
28
|
|
|
26
29
|
function configPaths() {
|
|
27
30
|
const directory = process.env.SIGNALDASH_HOME || `${homedir()}/.signaldash`;
|
|
@@ -70,9 +73,20 @@ export async function cmdLogin(code, backend, dependencies = {}) {
|
|
|
70
73
|
cfg.backend = backend || cfg.backend || DEFAULT_BACKEND;
|
|
71
74
|
saveCfg(cfg);
|
|
72
75
|
const r = await request("/login", { code }, { auth: false });
|
|
73
|
-
if (r.status !== 200) {
|
|
76
|
+
if (r.status !== 200) {
|
|
77
|
+
const msg = String(r.json.error || r.status);
|
|
78
|
+
if (/used|invalid/i.test(msg) && loadCfg().token) {
|
|
79
|
+
log("You are already set up on this machine. Run `signaldash status` to see what is connected.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
error("login failed:", msg);
|
|
83
|
+
if (/used/i.test(msg)) error("Invite codes are single-use. If you already ran this, try: signaldash status");
|
|
84
|
+
process.exitCode = 1; return;
|
|
85
|
+
}
|
|
74
86
|
cfg.token = r.json.token; saveCfg(cfg);
|
|
75
|
-
log(
|
|
87
|
+
log(`Logged in to SignalDash (${cfg.backend}).`);
|
|
88
|
+
log("Your LinkedIn/WhatsApp/email credentials live on that server, not on this");
|
|
89
|
+
log("machine, and are never exposed to your agent. Only you can see your data.");
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
export async function cmdLogout(dependencies = {}) {
|
|
@@ -378,6 +392,22 @@ export async function cmdConnections(outPath, dependencies = {}) {
|
|
|
378
392
|
if (!done) log(" " + chalk.dim("sync still running; run this again later to get the rest"));
|
|
379
393
|
}
|
|
380
394
|
|
|
395
|
+
|
|
396
|
+
function printHelp(log = console.log) {
|
|
397
|
+
log(`SignalDash \u2014 secure LinkedIn, WhatsApp and email access for your AI agent.
|
|
398
|
+
|
|
399
|
+
signaldash <invite-code> set up everything in one go
|
|
400
|
+
signaldash status show what is connected
|
|
401
|
+
signaldash connect linkedin|whatsapp|email
|
|
402
|
+
signaldash connections [file.csv] export your LinkedIn connections
|
|
403
|
+
signaldash skill install the agent skill
|
|
404
|
+
signaldash mcp run the MCP server (used by your agent)
|
|
405
|
+
signaldash logout revoke this device
|
|
406
|
+
|
|
407
|
+
Your channel credentials stay on the SignalDash server. They are never stored on
|
|
408
|
+
this machine and never shown to your agent, which only holds a scoped token.`);
|
|
409
|
+
}
|
|
410
|
+
|
|
381
411
|
export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
382
412
|
const [cmd, a, b, c] = argv;
|
|
383
413
|
const log = dependencies.log || console.log;
|
|
@@ -390,9 +420,10 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
390
420
|
else if (cmd === "mcp") await runMcp(dependencies);
|
|
391
421
|
else if (cmd === "status") await cmdStatus(dependencies);
|
|
392
422
|
else if (cmd === "connections" || (cmd === "export" && a === "connections")) await cmdConnections(cmd === "export" ? b : a, dependencies);
|
|
393
|
-
else if (cmd === "--version" || cmd === "-v") log(
|
|
394
|
-
else if (cmd && cmd !== "help" && !/^[0-9a-f]{8,}$/i.test(cmd)) { (dependencies.error || console.error)(`unknown command: ${cmd}`); printHelp(log); process.exitCode = 1; }
|
|
423
|
+
else if (cmd === "--version" || cmd === "-v") log(PACKAGE_VERSION);
|
|
395
424
|
else if (cmd === "skill") await cmdSkill(dependencies);
|
|
425
|
+
else if (cmd === "logout") await cmdLogout(dependencies);
|
|
426
|
+
else if (cmd && !["help","--help","-h"].includes(cmd) && !/^[0-9a-f]{8,}$/i.test(cmd)) { (dependencies.error || console.error)(`unknown command: ${cmd}`); printHelp(log); process.exitCode = 1; }
|
|
396
427
|
else printHelp(log);
|
|
397
428
|
}
|
|
398
429
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floomhq/signaldash",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Secure LinkedIn and
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Secure LinkedIn, WhatsApp, and email access for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"signaldash": "bin/sd.mjs"
|
|
@@ -1,103 +1,638 @@
|
|
|
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", "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, connection diagnosis, safe reading, drafting, approved sends, 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
|
+
- "Who engaged with my last post?", "who liked it?", "what did people comment?",
|
|
95
|
+
or "which warm signals need action?"
|
|
96
|
+
- "Export my connections", "download my LinkedIn network", or "make me a
|
|
97
|
+
connections CSV."
|
|
98
|
+
|
|
99
|
+
Do not use SignalDash for public LinkedIn research, invitations, profile
|
|
100
|
+
enrichment, scraping, bulk outreach, or a new email to someone with no existing
|
|
101
|
+
thread. SignalDash exposes no LinkedIn invitation tool.
|
|
102
|
+
|
|
103
|
+
## The operating model
|
|
104
|
+
|
|
105
|
+
There are two surfaces:
|
|
106
|
+
|
|
107
|
+
1. The CLI handles login, account connection, status, skill installation, MCP
|
|
108
|
+
startup, logout, and the paced LinkedIn connections export.
|
|
109
|
+
2. MCP tools handle account reads, message sends, email, LinkedIn posts, and
|
|
110
|
+
post engagement.
|
|
111
|
+
|
|
112
|
+
The CLI command used by the MCP registration is:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npx -y @floomhq/signaldash mcp
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
MCP tool results arrive as JSON text. An MCP result marked `isError: true`, an
|
|
119
|
+
HTTP-style `code`, or an `error` field is not a successful action.
|
|
120
|
+
|
|
121
|
+
## First-time setup
|
|
122
|
+
|
|
123
|
+
The user needs a single-use SignalDash invite code from Federico. Ask the user
|
|
124
|
+
for that code when none is present. Never invent one, search private files for
|
|
125
|
+
one, or reuse somebody else's code.
|
|
126
|
+
|
|
127
|
+
### 1. Run the one-command setup
|
|
128
|
+
|
|
129
|
+
After the user provides the code, run:
|
|
16
130
|
|
|
17
131
|
```bash
|
|
18
132
|
npx -y @floomhq/signaldash <invite-code>
|
|
19
133
|
```
|
|
20
134
|
|
|
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
|
-
|
|
135
|
+
The invite is a hexadecimal string. The command logs in, installs the bundled
|
|
136
|
+
skill, registers the MCP server with Claude Code when available, and begins
|
|
137
|
+
LinkedIn and WhatsApp connection.
|
|
138
|
+
|
|
139
|
+
Expected initial output resembles:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
SignalDash secure LinkedIn + WhatsApp access for your agent
|
|
143
|
+
|
|
144
|
+
+ logged in
|
|
145
|
+
+ agent skill installed
|
|
146
|
+
+ MCP registered with Claude Code
|
|
147
|
+
|
|
148
|
+
Opening your browser. If it does not open, use this link:
|
|
149
|
+
https://account.unipile.com/...
|
|
150
|
+
|
|
151
|
+
ACTION REQUIRED (human): open the link above to connect linkedin.
|
|
152
|
+
Then run: npx @floomhq/signaldash connect linkedin
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
In a non-interactive agent shell, the command prints hosted-auth links. The
|
|
156
|
+
agent cannot complete those pages, sign in as the user, or scan a WhatsApp QR
|
|
157
|
+
code.
|
|
158
|
+
|
|
159
|
+
### 2. Relay every printed link to the human
|
|
160
|
+
|
|
161
|
+
Tell the user which provider each link connects and what human action it needs:
|
|
162
|
+
|
|
163
|
+
- LinkedIn: open the hosted-auth link and finish LinkedIn authentication.
|
|
164
|
+
- WhatsApp: open the link and scan the live QR code from WhatsApp > Linked
|
|
165
|
+
Devices.
|
|
166
|
+
- Email: run `npx -y @floomhq/signaldash connect email`, relay its link, and let
|
|
167
|
+
the human choose Google, Outlook, or IMAP when available.
|
|
168
|
+
|
|
169
|
+
Do not hide a link in a summary. Send the full URL in a clickable form. Do not
|
|
170
|
+
click, scan, enter passwords, or claim connection on the user's behalf.
|
|
171
|
+
|
|
172
|
+
### 3. Verify with status
|
|
173
|
+
|
|
174
|
+
After the human says authentication is complete, run:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx -y @floomhq/signaldash status
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`status` is the source of truth after every `connect` attempt.
|
|
181
|
+
|
|
182
|
+
Connected output:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
SignalDash https://signaldash-api.floom.dev
|
|
186
|
+
+ linkedin Federico De Ponte
|
|
187
|
+
+ whatsapp Federico
|
|
188
|
+
- email not connected
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Not-connected output:
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
SignalDash https://signaldash-api.floom.dev
|
|
195
|
+
- linkedin not connected
|
|
196
|
+
- whatsapp not connected
|
|
197
|
+
- email not connected
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
A `connect` command printing another hosted-auth URL, even one that looks
|
|
201
|
+
identical to the previous output, does not prove the provider is connected.
|
|
202
|
+
Only the `+ <provider> <name>` line from `status`, or a direct
|
|
203
|
+
`Connected <provider>: <name>` result, proves connection.
|
|
204
|
+
|
|
205
|
+
### 4. Continue or diagnose each provider
|
|
206
|
+
|
|
207
|
+
Note: the one-command setup connects LinkedIn and WhatsApp. Email is opt-in and
|
|
208
|
+
needs its own call: `npx -y @floomhq/signaldash connect email`.
|
|
209
|
+
|
|
210
|
+
Run a missing provider explicitly:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx -y @floomhq/signaldash connect linkedin
|
|
214
|
+
npx -y @floomhq/signaldash connect whatsapp
|
|
215
|
+
npx -y @floomhq/signaldash connect email
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Interpret output as follows:
|
|
219
|
+
|
|
220
|
+
- Success: `Connected linkedin: <name>` or `status` shows `+ linkedin`.
|
|
221
|
+
- Pending human action: a hosted-auth URL plus `ACTION REQUIRED (human)`.
|
|
222
|
+
Relay the URL and wait for the human to finish.
|
|
223
|
+
- Still pending in an interactive terminal: `Not connected yet. The link above
|
|
224
|
+
stays valid...`. Do not treat the timeout as a failed login and do not create
|
|
225
|
+
a rapid loop of new links.
|
|
226
|
+
- Login failure: `Not logged in. Run: signaldash login <invite-code>` or
|
|
227
|
+
`login failed: ...`. Stop and resolve the session or invite.
|
|
228
|
+
- Provider failure: `connect failed: ...` or `connect status failed: ...`.
|
|
229
|
+
Report the exact error without claiming connection.
|
|
230
|
+
|
|
231
|
+
If automatic account detection cannot bind the newly authenticated account,
|
|
232
|
+
the CLI prints this manual fallback:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npx -y @floomhq/signaldash connect linkedin claim <account_id>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Use `claim` only with the exact account ID produced for this user's just-created
|
|
239
|
+
connection. Never guess an account ID.
|
|
240
|
+
|
|
241
|
+
### 5. Reload and prove tool availability
|
|
242
|
+
|
|
243
|
+
Restart the MCP connection or agent session after registration. Confirm the
|
|
244
|
+
expected SignalDash tools are present, then perform a read-only check such as:
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
li_list_chats({"limit": 5})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
If the tools are missing but `status` is connected, the account is ready and
|
|
251
|
+
the MCP client is not loaded. Fix the MCP registration or restart the client;
|
|
252
|
+
do not reconnect the account.
|
|
253
|
+
|
|
254
|
+
## Numbered workflow for every account task
|
|
255
|
+
|
|
256
|
+
Use this sequence unless the request is read-only and ends before approval:
|
|
257
|
+
|
|
258
|
+
1. **Confirm connection.** Run `npx -y @floomhq/signaldash status` when channel
|
|
259
|
+
state is unknown. A connected status is required.
|
|
260
|
+
2. **Choose the narrowest list tool.** List recent chats, email threads, or the
|
|
261
|
+
user's recent posts. Use a modest limit.
|
|
262
|
+
3. **Resolve the exact object.** Match the full chat name, thread, or post.
|
|
263
|
+
When names collide or identity is unclear, show the candidates and ask the
|
|
264
|
+
user. Never infer from a partial name.
|
|
265
|
+
4. **Read before interpreting.** Read enough recent history to understand the
|
|
266
|
+
latest inbound message, earlier context, and existing outbound messages.
|
|
267
|
+
For sends, read at least 10 recent items and use the exact chat or thread.
|
|
268
|
+
5. **Return findings or draft.** Summarize concrete facts. Separate suggested
|
|
269
|
+
replies from messages already sent.
|
|
270
|
+
6. **Obtain explicit approval.** Before any message or public post, show the
|
|
271
|
+
exact channel, recipient, and complete text. For email include subject and
|
|
272
|
+
body. Discussion, editing, "looks good", or approval of a different draft is
|
|
273
|
+
not approval of the final action.
|
|
274
|
+
7. **Re-read immediately before sending.** Re-read the exact thread to catch a
|
|
275
|
+
human reply, a manual send, or a duplicate that appeared after drafting. If
|
|
276
|
+
context changed, revise and obtain approval again.
|
|
277
|
+
8. **Act once.** Send one approved message or publish one approved post. Never
|
|
278
|
+
parallelize sends and never loop over recipients.
|
|
279
|
+
9. **Verify.** Read the exact thread again after a successful send. Confirm the
|
|
280
|
+
outbound text is present once. If the outcome was ambiguous, re-read before
|
|
281
|
+
any retry.
|
|
282
|
+
10. **Report guard state.** Include rate-limit remaining/reset information when
|
|
283
|
+
returned. Stop on warnings, 403, or 429.
|
|
284
|
+
|
|
285
|
+
Expected states:
|
|
286
|
+
|
|
287
|
+
- Read/list success: JSON containing `items`, often with a `cursor`.
|
|
288
|
+
- Draft success: `{"drafted":true,"published":false,...}`.
|
|
289
|
+
- Send success: provider result plus `rate_limit`, often including a message ID.
|
|
290
|
+
- Validation failure: `invalid_request` or a clear required-argument error.
|
|
291
|
+
- Safety rejection: 428, 409, or 429 as documented below.
|
|
292
|
+
- Provider warning/restriction: stop all activity on that account.
|
|
293
|
+
|
|
294
|
+
## Where your session lives
|
|
295
|
+
|
|
296
|
+
The CLI stores a SignalDash session token at `~/.signaldash/config.json`
|
|
297
|
+
(mode 0600). It is a scoped SignalDash token, NOT a LinkedIn/WhatsApp/email
|
|
298
|
+
credential: those stay on the server. Override the location with
|
|
299
|
+
`SIGNALDASH_HOME=/path` (useful for sandboxing or multiple accounts). Revoke a
|
|
300
|
+
device with `npx -y @floomhq/signaldash logout`.
|
|
301
|
+
|
|
302
|
+
## Tool catalog
|
|
303
|
+
|
|
304
|
+
Use the exact tool names and argument keys below. Limits are optional.
|
|
305
|
+
|
|
306
|
+
| Tool | Arguments | When to use it |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| `li_list_chats` | `limit` integer 1-100, default 20 | Find recent LinkedIn chats, unread counts, and exact `chat_id` values without opening profiles. |
|
|
309
|
+
| `li_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Read one resolved LinkedIn conversation before summarizing, drafting, or sending. |
|
|
310
|
+
| `li_send_message` | `chat_id` required; `text` required, max 5000 characters | Send one approved LinkedIn reply after an immediate read of that exact chat. |
|
|
311
|
+
| `wa_list_chats` | `limit` integer 1-100, default 20 | Find an existing WhatsApp conversation and exact `chat_id`. |
|
|
312
|
+
| `wa_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Verify a WhatsApp contact and recent history before summarizing, drafting, or sending. |
|
|
313
|
+
| `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. |
|
|
314
|
+
| `email_list` | `limit` integer 1-100, default 20 | List the newest message in each recent email thread and obtain `thread_id`. |
|
|
315
|
+
| `email_read` | `thread_id` required; `limit` 1-100, default 30 | Read an email thread and authorize its exact participant addresses for a later send. |
|
|
316
|
+
| `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. |
|
|
317
|
+
| `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. |
|
|
318
|
+
| `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. |
|
|
319
|
+
| `li_post_comments` | `post_id` required; `limit` default 50, max 100 | Read comments and authors for one post; prioritize questions and substantive responses. |
|
|
320
|
+
| `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. |
|
|
321
|
+
|
|
322
|
+
Representative calls:
|
|
323
|
+
|
|
324
|
+
```text
|
|
325
|
+
li_list_chats({"limit":20})
|
|
326
|
+
li_read_messages({"chat_id":"chat_li_7f3a","limit":20})
|
|
327
|
+
li_send_message({"chat_id":"chat_li_7f3a","text":"Yes. I’ll send it this afternoon."})
|
|
328
|
+
wa_list_chats({"limit":20})
|
|
329
|
+
wa_read_messages({"chat_id":"chat_wa_91b2","limit":20})
|
|
330
|
+
wa_send_message({"chat_id":"chat_wa_91b2","text":"16:30 works. See you then."})
|
|
331
|
+
email_list({"limit":20})
|
|
332
|
+
email_read({"thread_id":"thread_email_c402","limit":30})
|
|
333
|
+
email_send({"to":["amina@example.com"],"subject":"Re: Case study","body":"Hi Amina,\n\nHere is the case study."})
|
|
334
|
+
li_my_posts({"limit":5})
|
|
335
|
+
li_post_reactions({"post_id":"post_urn_8821","limit":50})
|
|
336
|
+
li_post_comments({"post_id":"post_urn_8821","limit":50})
|
|
337
|
+
li_draft_post({"text":"Most agents need better context, not more autonomy."})
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
List and read success returns JSON with `items` and often a `cursor`. A message
|
|
341
|
+
send success returns a provider message ID and `rate_limit` data. The draft
|
|
342
|
+
result is deterministic:
|
|
343
|
+
|
|
344
|
+
```json
|
|
345
|
+
{
|
|
346
|
+
"drafted": true,
|
|
347
|
+
"published": false,
|
|
348
|
+
"text": "Most agents need better context, not more autonomy.",
|
|
349
|
+
"note": "Draft only. Re-send with publish:true after the human approves."
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Publishing is public and irreversible. Never interpret "draft a post" as
|
|
354
|
+
permission to publish. `email_send` cannot start a cold thread and must never be
|
|
355
|
+
looped over recipients.
|
|
356
|
+
|
|
357
|
+
### LinkedIn connections export via CLI
|
|
358
|
+
|
|
359
|
+
This is the fourteenth operation. It is intentionally a paced CLI workflow, not
|
|
360
|
+
an MCP bulk-read tool.
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
npx -y @floomhq/signaldash connections linkedin-connections.csv
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Use it when the user asks to export or download their LinkedIn connections.
|
|
367
|
+
The server retrieves one page at a time in a resumable background sync. Do not
|
|
368
|
+
replace it with per-profile reads or parallel requests.
|
|
369
|
+
|
|
370
|
+
Progress and success resemble:
|
|
371
|
+
|
|
372
|
+
```text
|
|
373
|
+
Syncing connections (paced to keep your account safe)...
|
|
374
|
+
Synced 300 connections (3 pages)...
|
|
375
|
+
842 connections
|
|
376
|
+
+ saved linkedin-connections.csv
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
If the sync pauses, the CLI can save the rows fetched so far and prints that
|
|
380
|
+
the sync is still running. Running the same command later resumes it. If it
|
|
381
|
+
prints `nothing fetched yet, run again later to resume`, no CSV was produced
|
|
382
|
+
and the command exits with failure.
|
|
383
|
+
|
|
384
|
+
## Server-enforced guards: these will reject you
|
|
385
|
+
|
|
386
|
+
These controls run on the server. Prompt instructions, a new session, or a
|
|
387
|
+
different calling order cannot bypass them.
|
|
388
|
+
|
|
389
|
+
### 428 `read_before_send_required`
|
|
390
|
+
|
|
391
|
+
Meaning: this user has not successfully read the exact chat recently, or the
|
|
392
|
+
email recipient was not present in a recently read thread. The default read
|
|
393
|
+
window is 30 minutes.
|
|
394
|
+
|
|
395
|
+
Comply:
|
|
396
|
+
|
|
397
|
+
1. Call the matching read tool on the exact chat or email thread.
|
|
398
|
+
2. Inspect at least the last 10 items for identity, context, and duplicates.
|
|
399
|
+
3. If the context changed, revise the draft and obtain approval again.
|
|
400
|
+
4. Send once while the read is recent.
|
|
401
|
+
|
|
402
|
+
Do not satisfy this guard by reading a different chat with a similar name.
|
|
403
|
+
|
|
404
|
+
### 409 `duplicate_send`
|
|
405
|
+
|
|
406
|
+
Meaning: the exact message was already sent to that chat, or the exact email
|
|
407
|
+
recipient, subject, and body combination was already sent.
|
|
408
|
+
|
|
409
|
+
Comply:
|
|
410
|
+
|
|
411
|
+
1. Do not retry.
|
|
412
|
+
2. Re-read the exact thread or sent history.
|
|
413
|
+
3. Tell the user when the matching outbound item was sent, using `sent_at` when
|
|
414
|
+
returned.
|
|
415
|
+
4. Draft different text only when the user has a genuine follow-up intent, then
|
|
416
|
+
obtain new approval.
|
|
417
|
+
|
|
418
|
+
Changing whitespace or punctuation to evade the duplicate guard is prohibited.
|
|
419
|
+
|
|
420
|
+
### 429 `rate_limit_exceeded`
|
|
421
|
+
|
|
422
|
+
Meaning: the persisted daily action cap is exhausted. The response includes
|
|
423
|
+
`rate_limit.limit`, `used`, `remaining`, and `resets_at`, plus `Retry-After` and
|
|
424
|
+
matching rate-limit headers.
|
|
425
|
+
|
|
426
|
+
Comply:
|
|
427
|
+
|
|
428
|
+
1. Stop all sends on that account.
|
|
429
|
+
2. Report the limit and reset time.
|
|
430
|
+
3. Wait until the reset. Do not switch tokens, sessions, channels, or machines
|
|
431
|
+
to work around the cap.
|
|
432
|
+
4. Do not queue a burst for the reset boundary.
|
|
433
|
+
|
|
434
|
+
Any upstream 429, provider warning, checkpoint, restriction, unusual-activity
|
|
435
|
+
prompt, or HTTP 403 also means stop. Do not retry.
|
|
436
|
+
|
|
437
|
+
## Before every send
|
|
438
|
+
|
|
439
|
+
These rules apply to LinkedIn, WhatsApp, email, and public LinkedIn posts.
|
|
440
|
+
|
|
441
|
+
1. Never send or publish without explicit human approval of the exact
|
|
442
|
+
recipient or audience and the exact final text.
|
|
443
|
+
2. Read the exact thread immediately before the action. Check the recipient,
|
|
444
|
+
latest inbound message, prior context, and whether the proposed text already
|
|
445
|
+
exists as an outbound item.
|
|
446
|
+
3. Never infer a recipient from a partial name. Resolve duplicate names with
|
|
447
|
+
the user.
|
|
448
|
+
4. Never bulk-send, fan out, loop over people, or parallelize actions.
|
|
449
|
+
5. Never turn reactions, comments, connections, or exported rows into an
|
|
450
|
+
unsolicited outreach list.
|
|
451
|
+
6. Send one message at human pace. Let server pacing finish.
|
|
452
|
+
7. Stop on a provider warning, checkpoint, restriction, authentication
|
|
453
|
+
anomaly, delivery anomaly, HTTP 403, or HTTP 429.
|
|
454
|
+
8. Never retry an ambiguous timeout before re-reading the exact thread or sent
|
|
455
|
+
history. A timeout can hide a successful send.
|
|
456
|
+
9. Confirm delivery by reading the thread after the action.
|
|
457
|
+
10. Account health outranks throughput and task completion.
|
|
458
|
+
|
|
459
|
+
The hosted backend defaults to 20 action attempts per authenticated user per
|
|
460
|
+
UTC day, shared across message sends, email sends, and post publishing. A
|
|
461
|
+
deployment can configure a different cap. Never promise a particular remaining
|
|
462
|
+
allowance until the response reports `rate_limit.limit` and `remaining`.
|
|
463
|
+
|
|
464
|
+
## Worked flows
|
|
465
|
+
|
|
466
|
+
### Flow 1: triage unread LinkedIn chats and draft replies
|
|
467
|
+
|
|
468
|
+
User: "Check my unread LinkedIn chats and draft replies."
|
|
469
|
+
|
|
470
|
+
1. Confirm LinkedIn is connected with `signaldash status` when unknown.
|
|
471
|
+
2. Call:
|
|
472
|
+
|
|
473
|
+
```text
|
|
474
|
+
li_list_chats({"limit":20})
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
3. Select only items with `unread_count > 0`. Do not claim that the list tool
|
|
478
|
+
alone contains full conversation context.
|
|
479
|
+
4. Read each selected conversation one at a time:
|
|
480
|
+
|
|
481
|
+
```text
|
|
482
|
+
li_read_messages({"chat_id":"chat_li_7f3a","limit":20})
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
5. Return a compact triage:
|
|
486
|
+
|
|
487
|
+
```text
|
|
488
|
+
Amina Rahman, 2 unread
|
|
489
|
+
Last message: asks for the case study.
|
|
490
|
+
Suggested reply: "Yes. I’ll send it over this afternoon."
|
|
491
|
+
|
|
492
|
+
Marco Silva, 1 unread
|
|
493
|
+
Last message: confirms Tuesday at 10:00.
|
|
494
|
+
Suggested reply: "Tuesday at 10:00 works. See you then."
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
6. Stop. Drafting is not sending. Ask for approval of each exact reply.
|
|
498
|
+
7. For any approved reply, re-read that exact chat, send once, and read again
|
|
499
|
+
to verify. Do not send all drafts in a loop.
|
|
500
|
+
|
|
501
|
+
### Flow 2: find who engaged with the last post and recommend action
|
|
502
|
+
|
|
503
|
+
User: "Who engaged with my last LinkedIn post, and what should I do?"
|
|
504
|
+
|
|
505
|
+
1. Call:
|
|
506
|
+
|
|
507
|
+
```text
|
|
508
|
+
li_my_posts({"limit":5})
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
2. Select the newest post by its provider timestamp, not by list position when
|
|
512
|
+
timestamps disagree. Record its exact `post_id`.
|
|
513
|
+
3. Call:
|
|
514
|
+
|
|
515
|
+
```text
|
|
516
|
+
li_post_reactions({"post_id":"post_urn_8821","limit":50})
|
|
517
|
+
li_post_comments({"post_id":"post_urn_8821","limit":50})
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
4. Group the result:
|
|
521
|
+
|
|
522
|
+
- Direct questions in comments: respond publicly or draft a reply.
|
|
523
|
+
- Existing contacts with substantive engagement: warm follow-up candidate,
|
|
524
|
+
but no message without thread verification and approval.
|
|
525
|
+
- Reactions without context: signal only, no automatic outreach.
|
|
526
|
+
- Spam or irrelevant engagement: no action.
|
|
527
|
+
|
|
528
|
+
5. Return names, the evidence for prioritization, and proposed next actions.
|
|
529
|
+
Do not invent profile facts absent from the result.
|
|
530
|
+
6. If the user asks to message one person, locate the exact existing chat,
|
|
531
|
+
read it, draft a contextual message, obtain exact approval, re-read, and
|
|
532
|
+
send once. SignalDash cannot invite or mass-message reactors.
|
|
533
|
+
|
|
534
|
+
### Flow 3: find and reply to a WhatsApp thread
|
|
535
|
+
|
|
536
|
+
User: "Find my WhatsApp thread with Sara and reply that 16:30 works."
|
|
537
|
+
|
|
538
|
+
1. Call:
|
|
539
|
+
|
|
540
|
+
```text
|
|
541
|
+
wa_list_chats({"limit":20})
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
2. Resolve the exact Sara. If multiple chats or group/direct variants match,
|
|
545
|
+
show the candidates and ask which one.
|
|
546
|
+
3. Read:
|
|
547
|
+
|
|
548
|
+
```text
|
|
549
|
+
wa_read_messages({"chat_id":"chat_wa_91b2","limit":20})
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
4. Confirm that the latest context is about scheduling and draft:
|
|
553
|
+
|
|
554
|
+
```text
|
|
555
|
+
Recipient: Sara Conti, WhatsApp
|
|
556
|
+
Text: "16:30 works for me. See you then."
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
5. Ask for exact approval unless the user's original request already approved
|
|
560
|
+
that exact recipient and exact final text. Paraphrasing the user's words
|
|
561
|
+
creates a new draft that needs approval.
|
|
562
|
+
6. Immediately re-read `chat_wa_91b2`.
|
|
563
|
+
7. Send once:
|
|
564
|
+
|
|
565
|
+
```text
|
|
566
|
+
wa_send_message({
|
|
567
|
+
"chat_id":"chat_wa_91b2",
|
|
568
|
+
"text":"16:30 works for me. See you then."
|
|
569
|
+
})
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
8. Read the chat again and confirm the text appears once as an outbound
|
|
573
|
+
message.
|
|
574
|
+
|
|
575
|
+
### Flow 4: export LinkedIn connections
|
|
576
|
+
|
|
577
|
+
User: "Export my LinkedIn connections to CSV."
|
|
578
|
+
|
|
579
|
+
1. Confirm LinkedIn is connected.
|
|
580
|
+
2. Choose a user-visible output path. Never overwrite an existing file without
|
|
581
|
+
explicit authorization.
|
|
582
|
+
3. Run:
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
npx -y @floomhq/signaldash connections linkedin-connections.csv
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
4. Let the paced sync run. Do not start another export in parallel.
|
|
589
|
+
5. Success is the `+ saved <path>` line and a non-empty CSV with this header:
|
|
590
|
+
|
|
591
|
+
```text
|
|
592
|
+
name,headline,public_id,profile_url,connected_at
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
6. Report the saved path and row count. If the CLI reports a paused or partial
|
|
596
|
+
sync, say that the export is partial and rerun later to resume.
|
|
597
|
+
|
|
598
|
+
## Read-only requests and drafts
|
|
599
|
+
|
|
600
|
+
Read-only work does not require send approval. You can list, read, summarize,
|
|
601
|
+
analyze engagement, and create a non-published post draft as requested. Still
|
|
602
|
+
use narrow limits, protect private content, and avoid bulk collection.
|
|
603
|
+
|
|
604
|
+
For every draft, label it clearly as a draft. Never present a suggested reply
|
|
605
|
+
as sent. Never present `{"drafted":true,"published":false}` as a published post.
|
|
606
|
+
|
|
607
|
+
## Logout
|
|
608
|
+
|
|
609
|
+
When the user explicitly asks to disconnect this local SignalDash session, run:
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
npx -y @floomhq/signaldash logout
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Success:
|
|
616
|
+
|
|
617
|
+
```text
|
|
618
|
+
Logged out of SignalDash. The session was revoked.
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
Logout revokes the current SignalDash session and removes its local token. It
|
|
622
|
+
does not authorize deleting provider accounts or messages.
|
|
623
|
+
|
|
624
|
+
## Final checklist
|
|
625
|
+
|
|
626
|
+
Before reporting completion, verify:
|
|
627
|
+
|
|
628
|
+
- The skill is persisted in a discovered skill directory.
|
|
629
|
+
- `status` proves the required provider is connected.
|
|
630
|
+
- The exact chat, thread, post, or export path was resolved.
|
|
631
|
+
- Every send used a fresh exact-thread read and exact human approval.
|
|
632
|
+
- No duplicate, bulk, parallel, warning, 403, or 429 path was bypassed.
|
|
633
|
+
- A send was confirmed by a post-send read.
|
|
634
|
+
- A post draft remained unpublished unless `publish:true` was explicitly
|
|
635
|
+
approved.
|
|
636
|
+
- An export ended with `+ saved` and a non-empty CSV.
|
|
637
|
+
|
|
638
|
+
If any item is unverified, state exactly what remains incomplete.
|