@floomhq/signaldash 0.31.0 → 0.32.2
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 +101 -14
- package/bin/sd.mjs +161 -22
- package/package.json +2 -2
- package/skills/secretary/SKILL.md +66 -0
- package/skills/signaldash/SKILL.md +67 -41
- package/skills/signaldash-safe-usage/SKILL.md +19 -1
package/README.md
CHANGED
|
@@ -96,11 +96,13 @@ the same logged-in user.
|
|
|
96
96
|
The canonical [`signaldash` skill](skills/signaldash/SKILL.md) teaches an agent
|
|
97
97
|
when to use SignalDash, how to persist the skill, complete setup, interpret
|
|
98
98
|
connection state, operate every tool, handle server guards, and run safe worked
|
|
99
|
-
flows. The
|
|
99
|
+
flows. The thin [`secretary` skill](skills/secretary/SKILL.md) renders the
|
|
100
|
+
stored `latest` result as one table and has no write authority. The one-command
|
|
101
|
+
setup installs both from the same pinned npm package
|
|
100
102
|
the human chose to execute:
|
|
101
103
|
|
|
102
104
|
```bash
|
|
103
|
-
npx -y @floomhq/signaldash@0.
|
|
105
|
+
npx -y @floomhq/signaldash@0.32.2 <invite-code>
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
Run that command in a terminal, not in an agent chat. Do not ask an agent to
|
|
@@ -117,9 +119,14 @@ available for clients that separate operating and send-safety instructions.
|
|
|
117
119
|
|
|
118
120
|
SignalDash exposes:
|
|
119
121
|
|
|
122
|
+
- `sd_version()`
|
|
123
|
+
- `sd_secretary_latest(limit?, continuation?, ack_token?)`
|
|
124
|
+
- `sd_secretary_approve(disposition_id, payload_hash, confirm)`
|
|
125
|
+
- `sd_secretary_reject(disposition_id, confirm)`
|
|
120
126
|
- `li_list_chats`
|
|
121
127
|
- `li_read_messages(chat_id)`
|
|
122
|
-
- `
|
|
128
|
+
- `li_mark_read(chat_id)`
|
|
129
|
+
- `li_send_message(chat_id?, text?, expected_watermark?, mark_read?, secretary_receipt_id?)`
|
|
123
130
|
- `li_send_invitation(provider_id, note?, confirm?)`
|
|
124
131
|
- `li_invitations_received(limit?, cursor?)`
|
|
125
132
|
- `li_accept_invitation(invitation_id, confirm)`
|
|
@@ -147,17 +154,21 @@ SignalDash exposes:
|
|
|
147
154
|
- `sd_withdrawal_batch_cancel(withdrawal_batch_id, approval_view_hash, confirm)`
|
|
148
155
|
- `wa_list_chats`
|
|
149
156
|
- `wa_read_messages(chat_id)`
|
|
157
|
+
- `wa_mark_read(chat_id)`
|
|
150
158
|
- `wa_get_attachment(chat_id, message_id, attachment_id)`
|
|
151
159
|
- `wa_transcribe_voice(chat_id, message_id, attachment_id)`
|
|
152
|
-
- `wa_send_message(chat_id, text?, attachments?)`
|
|
160
|
+
- `wa_send_message(chat_id, text?, attachments?, expected_watermark?, mark_read?)`
|
|
153
161
|
- `wa_delete_message(chat_id, message_id)`
|
|
154
162
|
- `wa_delete_messages(messages)`
|
|
155
163
|
- `email_list(limit)`
|
|
156
164
|
- `email_read(thread_id, limit)`
|
|
157
165
|
- `email_send(to, subject, body)`
|
|
158
166
|
- `li_my_posts(limit, member_id)`
|
|
159
|
-
- `li_post_reactions(post_id, limit)`
|
|
160
|
-
- `li_post_comments(post_id, limit)`
|
|
167
|
+
- `li_post_reactions(post_id, limit, cursor?)`
|
|
168
|
+
- `li_post_comments(post_id, comment_id?, limit, cursor?)`
|
|
169
|
+
- `li_reply_to_comment(post_id?, parent_comment_id?, trigger_comment_id?, text?, expected_watermark?, secretary_receipt_id?)`
|
|
170
|
+
- `li_delete_message(chat_id, message_id, confirm)`
|
|
171
|
+
- `li_delete_comment(post_id, comment_id, confirm)`
|
|
161
172
|
- `li_draft_post(text, publish, scheduled_at?, mentions?, attachments?, first_comment?)`
|
|
162
173
|
- `li_set_scheduled_post_first_comment(id, first_comment, confirm)`
|
|
163
174
|
- `li_scheduled_posts()`
|
|
@@ -169,6 +180,24 @@ SignalDash exposes:
|
|
|
169
180
|
Every operation runs through the hosted SignalDash backend. Agents never
|
|
170
181
|
receive the Unipile access key.
|
|
171
182
|
|
|
183
|
+
`li_reply_to_comment` acts only on an inbound comment on the authenticated
|
|
184
|
+
sender's own post. A preceding `li_post_comments` read records an exact
|
|
185
|
+
per-comment watermark. Immediately before the write, SignalDash re-proves the
|
|
186
|
+
post owner, sender generation, trigger author, unchanged trigger, parent
|
|
187
|
+
thread, absence of an own duplicate, action budget and sender health. A
|
|
188
|
+
provider 2xx remains `outcome_unknown` until bounded readback finds exactly one
|
|
189
|
+
own reply with the returned reply ID, expected parent and exact text.
|
|
190
|
+
|
|
191
|
+
LinkedIn deletion is remediation after exposure, not rollback. It never
|
|
192
|
+
relaxes a send or Secretary gate. `li_delete_message` is limited to the first
|
|
193
|
+
60 minutes after the provider send timestamp and proves exact account, chat,
|
|
194
|
+
message and own authorship before using a separate remediation budget. It
|
|
195
|
+
marks success only after a readback proves the message absent. The v2
|
|
196
|
+
`li_delete_comment` wrapper is installed behind the database capability flag
|
|
197
|
+
`linkedin_comment_delete`, whose migration default is `untested`. It is not an
|
|
198
|
+
available live capability until an exact removable comment on Federico's own
|
|
199
|
+
post passes compatibility testing and the flag is separately enabled.
|
|
200
|
+
|
|
172
201
|
A WhatsApp message can carry files. `wa_send_message` takes up to 4 exact
|
|
173
202
|
`{filename, content_type, content_base64}` attachments, at most 16 MiB per file
|
|
174
203
|
and 16 MiB per message, and accepts PNG, JPEG, WebP, GIF, PDF, CSV, plain text,
|
|
@@ -182,16 +211,23 @@ oversized attachment costs no send budget and never reaches the provider, and
|
|
|
182
211
|
every refusal names the exact rule and the exact file that tripped it. Nothing
|
|
183
212
|
is ever truncated or dropped silently. LinkedIn messages carry text only, and
|
|
184
213
|
`li_send_message` refuses an `attachments` argument instead of ignoring it.
|
|
214
|
+
For a Secretary draft, `sd_secretary_approve` creates one 15-minute receipt
|
|
215
|
+
bound to the exact stored sender generation, recipient, thread, and text.
|
|
216
|
+
`li_send_message` then accepts that receipt ID alone, derives the payload from
|
|
217
|
+
storage, refuses caller-supplied overrides, and consumes the receipt atomically
|
|
218
|
+
with the action reservation. Rejection records a new human disposition and
|
|
219
|
+
sends nothing.
|
|
185
220
|
|
|
186
221
|
A recent read is no longer sufficient on its own. A message send re-reads the
|
|
187
|
-
thread immediately before sending
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
222
|
+
thread immediately before sending. A counterparty message or a mutation after
|
|
223
|
+
the read refuses with `409 thread_changed`; an addition consisting only of this
|
|
224
|
+
account's own outbound messages does not invalidate the draft. The 409 includes
|
|
225
|
+
`new_messages`, `changed_kind`, and `current_watermark`, and both send tools
|
|
226
|
+
accept optional `expected_watermark` to bind a retry to the exact state the
|
|
227
|
+
caller reviewed. A 30-minute read window is satisfied just as comfortably by a
|
|
228
|
+
conversation that moved 29 minutes ago as by one nobody has touched, and a
|
|
229
|
+
reply that arrived in between is precisely when a draft stops being the right
|
|
230
|
+
thing to send. A chat whose most recent read predates this guard carries no hash
|
|
195
231
|
and is refused with `428 read_before_send_required` until it is read again,
|
|
196
232
|
rather than waved through on a timestamp that cannot answer the question. If the
|
|
197
233
|
re-read itself fails, the send is refused with
|
|
@@ -419,6 +455,31 @@ resume exactly those. WhatsApp applies its own time and role limits to deleting
|
|
|
419
455
|
for everyone and can answer successfully without removing anything, so re-read
|
|
420
456
|
the chat to confirm.
|
|
421
457
|
|
|
458
|
+
## Deploying server changes
|
|
459
|
+
|
|
460
|
+
A server change is not landed when only the files on disk changed. After the
|
|
461
|
+
change is merged to `main`, update the production checkout, restart the
|
|
462
|
+
long-running backend, and compare the live MCP result with the checked-out
|
|
463
|
+
commit:
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
git pull --ff-only origin main
|
|
467
|
+
git rev-parse HEAD
|
|
468
|
+
sudo systemctl restart signaldash-api.service
|
|
469
|
+
sudo systemctl is-active signaldash-api.service
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Then call `sd_version({})` from an authenticated MCP session. Its `commit_sha`
|
|
473
|
+
must equal `git rev-parse HEAD`, and its `process_started_at` must be later than
|
|
474
|
+
the restart. The startup journal contains the same snapshot under the
|
|
475
|
+
`[signaldash] startup` prefix. A mismatch means the deployment is stale and the
|
|
476
|
+
server change is not shipped. `dirty_at_start` includes tracked changes,
|
|
477
|
+
untracked files, and submodule state present when the process started.
|
|
478
|
+
|
|
479
|
+
`SIGNALDASH_VERSION_ENABLED=0` removes the version route at runtime after a
|
|
480
|
+
service restart. It defaults to enabled and exposes no credential, account, or
|
|
481
|
+
message data.
|
|
482
|
+
|
|
422
483
|
## Releasing
|
|
423
484
|
|
|
424
485
|
A release is a tag push. `.github/workflows/release.yml` runs `./release.sh` on
|
|
@@ -471,6 +532,32 @@ npm run check
|
|
|
471
532
|
npm pack --dry-run
|
|
472
533
|
```
|
|
473
534
|
|
|
535
|
+
### Offline LinkedIn archive import
|
|
536
|
+
|
|
537
|
+
The server-side importer reads LinkedIn export directories or ZIP files without
|
|
538
|
+
making provider requests. Preview the exact file fingerprints and row counts,
|
|
539
|
+
then apply the same archive to one bound LinkedIn sender generation:
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
node scripts/import-linkedin-archive.mjs \
|
|
543
|
+
--archive /path/to/Complete_LinkedInDataExport \
|
|
544
|
+
--database server/sd-state-control.sqlite3 \
|
|
545
|
+
--sender-account <exact-sender-account-id> \
|
|
546
|
+
--owner-profile-url https://www.linkedin.com/in/<exact-owner-public-id>/ \
|
|
547
|
+
--dry-run
|
|
548
|
+
|
|
549
|
+
node scripts/import-linkedin-archive.mjs \
|
|
550
|
+
--archive /path/to/Complete_LinkedInDataExport \
|
|
551
|
+
--database server/sd-state-control.sqlite3 \
|
|
552
|
+
--sender-account <exact-sender-account-id> \
|
|
553
|
+
--owner-profile-url https://www.linkedin.com/in/<exact-owner-public-id>/ \
|
|
554
|
+
--apply
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
The owner profile URL is required when `messages.csv` is present. It is the
|
|
558
|
+
typed identity used to classify inbound and outbound rows. A display name is
|
|
559
|
+
never used for that decision.
|
|
560
|
+
|
|
474
561
|
`wa_transcribe_voice` runs on the SignalDash host and has two backends. The
|
|
475
562
|
default, `gemini`, sends the audio to Vertex AI using that host's own
|
|
476
563
|
credentials; it is the one that survives German speech with English technical
|
package/bin/sd.mjs
CHANGED
|
@@ -298,18 +298,72 @@ export async function cmdClaim(provider, accountId, dependencies = {}) {
|
|
|
298
298
|
|
|
299
299
|
// ---- MCP (stdio). Every tool proxies through the backend with the token. ----
|
|
300
300
|
const TOOLS = [
|
|
301
|
+
{
|
|
302
|
+
name: "sd_version",
|
|
303
|
+
path: "/sd/version",
|
|
304
|
+
description: "Report the exact SignalDash backend commit, working-tree state, process start time, and package version captured when the serving process started.",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {},
|
|
308
|
+
additionalProperties: false,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: "sd_secretary_latest",
|
|
313
|
+
path: "/sd/secretary/latest",
|
|
314
|
+
description: "Read stored Secretary signals with zero provider calls. Render rows and the ignored aggregate as one table. On the next call, pass the previous ack_token; pass next as continuation when it is non-null.",
|
|
315
|
+
inputSchema: {
|
|
316
|
+
type: "object",
|
|
317
|
+
properties: {
|
|
318
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
|
|
319
|
+
continuation: { type: "string", minLength: 1, maxLength: 4096 },
|
|
320
|
+
ack_token: { type: "string", minLength: 1, maxLength: 4096 },
|
|
321
|
+
},
|
|
322
|
+
additionalProperties: false,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "sd_secretary_approve",
|
|
327
|
+
path: "/sd/secretary/approve",
|
|
328
|
+
description: "Approve one exact stored Secretary draft. Pass only the disposition_id and payload_hash returned by sd_secretary_latest plus confirm:true. The result is a single-use 15-minute receipt.",
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: "object",
|
|
331
|
+
properties: {
|
|
332
|
+
disposition_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
333
|
+
payload_hash: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
334
|
+
confirm: { type: "boolean", const: true },
|
|
335
|
+
},
|
|
336
|
+
required: ["disposition_id", "payload_hash", "confirm"],
|
|
337
|
+
additionalProperties: false,
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "sd_secretary_reject",
|
|
342
|
+
path: "/sd/secretary/reject",
|
|
343
|
+
description: "Reject one exact current stored Secretary draft and create a durable rejected disposition. Sends nothing.",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: {
|
|
347
|
+
disposition_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
348
|
+
confirm: { type: "boolean", const: true },
|
|
349
|
+
},
|
|
350
|
+
required: ["disposition_id", "confirm"],
|
|
351
|
+
additionalProperties: false,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
301
354
|
{
|
|
302
355
|
name: "li_list_chats",
|
|
303
356
|
ch: "li",
|
|
304
357
|
action: "list_chats",
|
|
305
|
-
description: "List or search your LinkedIn chats
|
|
358
|
+
description: "List or search your LinkedIn chats by display name, public_id, member id, or chat fields. Pass unread:true to make the provider return unread chats only. A unique stored connection name or exact public_id is resolved to its verified LinkedIn member id before matching. A zero-match default search continues from 200 up to a hard 500-chat/five-page bound and reports the exact scan depth.",
|
|
306
359
|
inputSchema: {
|
|
307
360
|
type: "object",
|
|
308
361
|
properties: {
|
|
309
362
|
limit: { type: "integer", minimum: 1, maximum: 100, description: "How many chats to return. With `search`, how many MATCHES to return." },
|
|
310
363
|
cursor: { type: "string", maxLength: 4000, description: "Continue from a previous page or a previous search." },
|
|
311
|
-
search: { type: "string", minLength: 1, maxLength: 200, description: "Filter chats on
|
|
312
|
-
max_scan: { type: "integer", minimum: 1, maximum: 500, description: "
|
|
364
|
+
search: { type: "string", minLength: 1, maxLength: 200, description: "Filter chats on a unique stored connection display name, exact LinkedIn public_id, verified member id, or chat fields. SignalDash resolves a stored name/public_id to its member id before the bounded chat scan." },
|
|
365
|
+
max_scan: { type: "integer", minimum: 1, maximum: 500, description: "Explicit scan bound. Without it, SignalDash scans 200 chats and, only after zero matches with more pages available, continues up to 500. Every search is capped at five provider pages and reports the real depth." },
|
|
366
|
+
unread: { type: "boolean", description: "Ask the LinkedIn provider for unread chats only. true is forwarded as unread=true; omitted or false does not apply the provider filter." },
|
|
313
367
|
},
|
|
314
368
|
additionalProperties: false,
|
|
315
369
|
},
|
|
@@ -318,7 +372,7 @@ const TOOLS = [
|
|
|
318
372
|
name: "li_read_messages",
|
|
319
373
|
ch: "li",
|
|
320
374
|
action: "read",
|
|
321
|
-
description: "Read messages in
|
|
375
|
+
description: "Read messages and the resolved chat in one LinkedIn thread. Messages include sender_name where the exact sender can be resolved. args: chat_id",
|
|
322
376
|
inputSchema: {
|
|
323
377
|
type: "object",
|
|
324
378
|
properties: {
|
|
@@ -329,18 +383,34 @@ const TOOLS = [
|
|
|
329
383
|
additionalProperties: false,
|
|
330
384
|
},
|
|
331
385
|
},
|
|
386
|
+
{
|
|
387
|
+
name: "li_mark_read",
|
|
388
|
+
ch: "li",
|
|
389
|
+
action: "mark_read",
|
|
390
|
+
description: "Explicitly clear the unread state of one account-scoped LinkedIn chat. Reading a thread never clears it automatically.",
|
|
391
|
+
inputSchema: {
|
|
392
|
+
type: "object",
|
|
393
|
+
properties: {
|
|
394
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
395
|
+
},
|
|
396
|
+
required: ["chat_id"],
|
|
397
|
+
additionalProperties: false,
|
|
398
|
+
},
|
|
399
|
+
},
|
|
332
400
|
{
|
|
333
401
|
name: "li_send_message",
|
|
334
402
|
ch: "li",
|
|
335
403
|
action: "send",
|
|
336
|
-
description: "Send a LinkedIn message (rate-safe).
|
|
404
|
+
description: "Send a LinkedIn message (rate-safe). For a manual send, pass chat_id and text after reading the exact thread. For an approved Secretary draft, pass secretary_receipt_id alone; recipient and text are derived from storage and any override is refused. Own outbound messages added after a read do not invalidate the draft, but a counterparty message or mutation returns 409 thread_changed.",
|
|
337
405
|
inputSchema: {
|
|
338
406
|
type: "object",
|
|
339
407
|
properties: {
|
|
340
408
|
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
341
409
|
text: { type: "string", minLength: 1, maxLength: 5000 },
|
|
410
|
+
expected_watermark: { type: "string", minLength: 64, maxLength: 64, pattern: "^[0-9a-f]{64}$", description: "Bind the send to the exact current_watermark returned by the read or a thread_changed response." },
|
|
411
|
+
mark_read: { type: "boolean", default: false, description: "After a confirmed successful send, explicitly clear this chat's unread state." },
|
|
412
|
+
secretary_receipt_id: { type: "string", minLength: 1, maxLength: 200, description: "Single-use receipt from sd_secretary_approve. Pass this field alone." },
|
|
342
413
|
},
|
|
343
|
-
required: ["chat_id", "text"],
|
|
344
414
|
additionalProperties: false,
|
|
345
415
|
},
|
|
346
416
|
},
|
|
@@ -555,7 +625,7 @@ const TOOLS = [
|
|
|
555
625
|
{
|
|
556
626
|
name: "li_search_connections",
|
|
557
627
|
path: "/li/connections/search",
|
|
558
|
-
description: "Search only your locally stored LinkedIn connections by name, headline, or company and
|
|
628
|
+
description: "Search only your locally stored LinkedIn connections by name, headline, or company. Returns both public_id and the stable LinkedIn member_id when known, plus exact local contact and known-chat state. Makes zero LinkedIn, Unipile, or paid discovery API calls.",
|
|
559
629
|
inputSchema: {
|
|
560
630
|
type: "object",
|
|
561
631
|
properties: {
|
|
@@ -969,13 +1039,13 @@ const TOOLS = [
|
|
|
969
1039
|
name: "wa_list_chats",
|
|
970
1040
|
ch: "wa",
|
|
971
1041
|
action: "list_chats",
|
|
972
|
-
description: "List or search your WhatsApp chats.
|
|
1042
|
+
description: "List or search your WhatsApp chats. Resolved 1:1 chats expose the contact in both name and attendee_name; unresolved names stay null. A search reports exactly how far it scanned.",
|
|
973
1043
|
inputSchema: {
|
|
974
1044
|
type: "object",
|
|
975
1045
|
properties: {
|
|
976
1046
|
limit: { type: "integer", minimum: 1, maximum: 100, description: "How many chats to return. With `search`, how many MATCHES to return." },
|
|
977
1047
|
cursor: { type: "string", maxLength: 4000, description: "Continue from a previous page or a previous search." },
|
|
978
|
-
search: { type: "string", minLength: 1, maxLength: 200, description: "Filter chats on name and counterpart identifiers. The provider cannot filter a chat list, so SignalDash filters over a bounded scan and the response reports `scanned_chats` and `exhaustive` so a miss is never mistaken for a proven absence.
|
|
1048
|
+
search: { type: "string", minLength: 1, maxLength: 200, description: "Filter chats on resolved name and counterpart identifiers. The provider cannot filter a chat list, so SignalDash filters over a bounded scan and the response reports `scanned_chats` and `exhaustive` so a miss is never mistaken for a proven absence. Search by the exact phone number when a 1:1 name remains unresolved." },
|
|
979
1049
|
max_scan: { type: "integer", minimum: 1, maximum: 500, description: "How many chats the search may scan before it stops and reports exhaustive:false. Default 200, and never more than five provider pages: bulk reading is the top account-restriction trigger, so go deeper with the returned cursor rather than with a bigger scan." },
|
|
980
1050
|
},
|
|
981
1051
|
additionalProperties: false,
|
|
@@ -985,7 +1055,7 @@ const TOOLS = [
|
|
|
985
1055
|
name: "wa_read_messages",
|
|
986
1056
|
ch: "wa",
|
|
987
1057
|
action: "read",
|
|
988
|
-
description: "Read messages in
|
|
1058
|
+
description: "Read messages and the resolved chat in one WhatsApp thread. Messages include sender_name where the exact sender can be resolved. args: chat_id",
|
|
989
1059
|
inputSchema: {
|
|
990
1060
|
type: "object",
|
|
991
1061
|
properties: {
|
|
@@ -996,6 +1066,20 @@ const TOOLS = [
|
|
|
996
1066
|
additionalProperties: false,
|
|
997
1067
|
},
|
|
998
1068
|
},
|
|
1069
|
+
{
|
|
1070
|
+
name: "wa_mark_read",
|
|
1071
|
+
ch: "wa",
|
|
1072
|
+
action: "mark_read",
|
|
1073
|
+
description: "Explicitly clear the unread state of one account-scoped WhatsApp chat. Reading a thread never clears it automatically.",
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
type: "object",
|
|
1076
|
+
properties: {
|
|
1077
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1078
|
+
},
|
|
1079
|
+
required: ["chat_id"],
|
|
1080
|
+
additionalProperties: false,
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
999
1083
|
{
|
|
1000
1084
|
name: "wa_get_attachment",
|
|
1001
1085
|
path: "/wa/get_attachment",
|
|
@@ -1031,7 +1115,7 @@ const TOOLS = [
|
|
|
1031
1115
|
name: "wa_send_message",
|
|
1032
1116
|
ch: "wa",
|
|
1033
1117
|
action: "send",
|
|
1034
|
-
description: "Send a WhatsApp message (rate-safe). args: chat_id, text, attachments. Read the chat first: a send into a thread this account has not read recently is refused.
|
|
1118
|
+
description: "Send a WhatsApp message (rate-safe). args: chat_id, text, attachments, expected_watermark, mark_read. Read the chat first: the read returns current_watermark and a send into a thread this account has not read recently is refused. Own outbound messages added after that read do not invalidate the draft, but a counterparty message or mutation returns 409 thread_changed with new_messages, changed_kind, and current_watermark. Optional expected_watermark binds the send to that exact reviewed state. A re-read that fails is 502 thread_preflight_unavailable and nothing was sent. attachments optionally carries up to 4 base64 files as exact {filename, content_type, content_base64} objects, at most 16 MiB per file and 16 MiB per message, and accepts images, PDF, CSV, plain text, JSON, xlsx and zip. text is the caption and may be omitted when a file is attached, but a call carrying neither text nor an attachment is refused. Optional mark_read:true explicitly clears unread only after the send is confirmed. An attachment send is rate-limited, deduplicated and recorded exactly like a text send, and spends the same daily budget. If a send times out or the provider never confirms it, the message may still have been delivered: SignalDash records it and refuses an identical retry with 409 send_outcome_unknown. Read the chat, and only if the message is genuinely absent, resend the identical payload with confirm_resend:true.",
|
|
1035
1119
|
inputSchema: {
|
|
1036
1120
|
type: "object",
|
|
1037
1121
|
properties: {
|
|
@@ -1064,6 +1148,8 @@ const TOOLS = [
|
|
|
1064
1148
|
},
|
|
1065
1149
|
},
|
|
1066
1150
|
confirm_resend: { type: "boolean", const: true },
|
|
1151
|
+
expected_watermark: { type: "string", minLength: 64, maxLength: 64, pattern: "^[0-9a-f]{64}$", description: "Bind the send to the exact current_watermark returned by the read or a thread_changed response." },
|
|
1152
|
+
mark_read: { type: "boolean", default: false, description: "After a confirmed successful send, explicitly clear this chat's unread state." },
|
|
1067
1153
|
},
|
|
1068
1154
|
// `text` alone is no longer required: a document with no caption is a
|
|
1069
1155
|
// legitimate message.
|
|
@@ -1188,7 +1274,7 @@ const TOOLS = [
|
|
|
1188
1274
|
{
|
|
1189
1275
|
name: "li_my_posts",
|
|
1190
1276
|
path: "/li/posts",
|
|
1191
|
-
description: "List
|
|
1277
|
+
description: "List LinkedIn posts with engagement counts (reactions, comments, impressions). Use each row's exact social_id, not a different numeric id, with li_post_reactions and li_post_comments.",
|
|
1192
1278
|
inputSchema: {
|
|
1193
1279
|
type: "object",
|
|
1194
1280
|
properties: {
|
|
@@ -1201,12 +1287,13 @@ const TOOLS = [
|
|
|
1201
1287
|
{
|
|
1202
1288
|
name: "li_post_reactions",
|
|
1203
1289
|
path: "/li/post_reactions",
|
|
1204
|
-
description: "
|
|
1290
|
+
description: "Read one bounded page of people who reacted to a post. Pass the exact social_id returned by li_my_posts (including urn:li:ugcPost:... for multi-image posts); a numeric id is resolved against your own recent posts when possible. Read completeness.state, completeness.total, and completeness.next_cursor before treating the list as complete, then pass cursor to continue. The true total comes from the provider response when available; SignalDash does not invent one from li_my_posts. Reactions are signals only and do not authorize outreach.",
|
|
1205
1291
|
inputSchema: {
|
|
1206
1292
|
type: "object",
|
|
1207
1293
|
properties: {
|
|
1208
1294
|
post_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1209
1295
|
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
1296
|
+
cursor: { type: "string", minLength: 1, maxLength: 4000 },
|
|
1210
1297
|
},
|
|
1211
1298
|
required: ["post_id"],
|
|
1212
1299
|
additionalProperties: false,
|
|
@@ -1215,17 +1302,66 @@ const TOOLS = [
|
|
|
1215
1302
|
{
|
|
1216
1303
|
name: "li_post_comments",
|
|
1217
1304
|
path: "/li/post_comments",
|
|
1218
|
-
description: "
|
|
1305
|
+
description: "Read one bounded page of comments and authors on a post. Pass comment_id to read replies to that exact parent comment. Pass the exact social_id returned by li_my_posts; a numeric id is resolved against your own recent posts when possible. Read completeness.state, completeness.total, and completeness.next_cursor before treating the list as complete, then pass cursor to continue. The true total comes from the provider response when available; SignalDash never guesses it from li_my_posts.",
|
|
1219
1306
|
inputSchema: {
|
|
1220
1307
|
type: "object",
|
|
1221
1308
|
properties: {
|
|
1222
1309
|
post_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1310
|
+
comment_id: { type: "string", minLength: 1, maxLength: 500, description: "Optional exact parent comment id. When present, returns replies to that comment." },
|
|
1223
1311
|
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
1312
|
+
cursor: { type: "string", minLength: 1, maxLength: 4000 },
|
|
1224
1313
|
},
|
|
1225
1314
|
required: ["post_id"],
|
|
1226
1315
|
additionalProperties: false,
|
|
1227
1316
|
},
|
|
1228
1317
|
},
|
|
1318
|
+
{
|
|
1319
|
+
name: "li_reply_to_comment",
|
|
1320
|
+
path: "/li/reply_to_comment",
|
|
1321
|
+
description: "Reply once to one exact inbound comment on the authenticated sender's own LinkedIn post. First read li_post_comments and preserve the exact post_id, parent_comment_id, trigger_comment_id, text, and returned signaldash_watermark. SignalDash re-proves post ownership, trigger identity and content, absence of an own duplicate, sender generation, budget, and provider safety immediately before writing. A 2xx is not success until bounded readback finds exactly one own reply. A Secretary approval uses secretary_receipt_id alone.",
|
|
1322
|
+
inputSchema: {
|
|
1323
|
+
type: "object",
|
|
1324
|
+
properties: {
|
|
1325
|
+
post_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1326
|
+
parent_comment_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1327
|
+
trigger_comment_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1328
|
+
text: { type: "string", minLength: 1, maxLength: 1250 },
|
|
1329
|
+
expected_watermark: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
1330
|
+
secretary_receipt_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1331
|
+
},
|
|
1332
|
+
additionalProperties: false,
|
|
1333
|
+
},
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
name: "li_delete_message",
|
|
1337
|
+
path: "/li/delete_message",
|
|
1338
|
+
description: "Remediate one exact LinkedIn message sent by this authenticated account, only within the provider's 60-minute window. Requires exact chat and message identity plus confirm:true. SignalDash proves chat ownership, exact-chat membership, own authorship, timestamp eligibility, a separate remediation budget, and post-delete absence. Deletion cannot undo prior delivery, reading, or notifications and does not weaken any send gate.",
|
|
1339
|
+
inputSchema: {
|
|
1340
|
+
type: "object",
|
|
1341
|
+
properties: {
|
|
1342
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1343
|
+
message_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1344
|
+
confirm: { type: "boolean", const: true },
|
|
1345
|
+
},
|
|
1346
|
+
required: ["chat_id", "message_id", "confirm"],
|
|
1347
|
+
additionalProperties: false,
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
name: "li_delete_comment",
|
|
1352
|
+
path: "/li/delete_comment",
|
|
1353
|
+
description: "UNTESTED AND DISABLED by the database capability flag. The guarded Unipile v2 wrapper is fixture-tested but is unavailable until live compatibility is proved against the authenticated sender's own removable comment. When enabled it requires exact post_id, comment_id, confirm:true, own-comment proof, a separate remediation budget, and post-delete readback. Deletion is remediation, not rollback.",
|
|
1354
|
+
inputSchema: {
|
|
1355
|
+
type: "object",
|
|
1356
|
+
properties: {
|
|
1357
|
+
post_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1358
|
+
comment_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1359
|
+
confirm: { type: "boolean", const: true },
|
|
1360
|
+
},
|
|
1361
|
+
required: ["post_id", "comment_id", "confirm"],
|
|
1362
|
+
additionalProperties: false,
|
|
1363
|
+
},
|
|
1364
|
+
},
|
|
1229
1365
|
{
|
|
1230
1366
|
name: "li_draft_post",
|
|
1231
1367
|
path: "/li/create_post",
|
|
@@ -1408,20 +1544,23 @@ export async function cmdSkill(dependencies = {}) {
|
|
|
1408
1544
|
const { mkdirSync: mk, writeFileSync: wf, readFileSync: rf, existsSync: ex } = await import("node:fs");
|
|
1409
1545
|
const { dirname, join } = await import("node:path");
|
|
1410
1546
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1411
|
-
const
|
|
1412
|
-
if (!ex(src)) { log("skill file not found in package"); process.exitCode = 1; return; }
|
|
1413
|
-
const dest = join(homedir(), ".claude", "skills", "signaldash");
|
|
1414
|
-
mk(dest, { recursive: true });
|
|
1547
|
+
const skills = ["signaldash", "secretary"];
|
|
1415
1548
|
// The template pins the bootstrap command to a version placeholder rather
|
|
1416
1549
|
// than a typed-in string: package.json is the only source of truth, so an
|
|
1417
1550
|
// installed skill can never advertise a stale published release. Rendered
|
|
1418
1551
|
// by the shared helper (lib/skill-template.cjs) so this substitution has a
|
|
1419
1552
|
// single source of truth shared with the public skill page and the
|
|
1420
1553
|
// server's own /skill and /skill.md routes.
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1554
|
+
for (const name of skills) {
|
|
1555
|
+
const src = join(here, "..", "skills", name, "SKILL.md");
|
|
1556
|
+
if (!ex(src)) { log(`${name} skill file not found in package`); process.exitCode = 1; return; }
|
|
1557
|
+
const dest = join(homedir(), ".claude", "skills", name);
|
|
1558
|
+
mk(dest, { recursive: true });
|
|
1559
|
+
const template = rf(src, "utf8");
|
|
1560
|
+
wf(join(dest, "SKILL.md"), renderSkillTemplate(template, PACKAGE_VERSION));
|
|
1561
|
+
log(`Installed the ${name === "signaldash" ? "SignalDash" : "Secretary"} skill to ${dest}/SKILL.md`);
|
|
1562
|
+
}
|
|
1563
|
+
log("Your agent now knows how to use SignalDash safely and render Secretary latest.");
|
|
1425
1564
|
}
|
|
1426
1565
|
|
|
1427
1566
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floomhq/signaldash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.2",
|
|
4
4
|
"description": "Secure LinkedIn, WhatsApp, and email access for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"test": "node --test",
|
|
21
|
-
"check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check server/composio-email.cjs && node --check server/attachments.cjs && node --check server/write-control.cjs && node --check server/voice-profile.cjs && node --check server/campaign-store.cjs && node --check server/campaign-runner.cjs && node --check server/withdrawal-store.cjs && node --check server/post-scheduler.cjs && node --check server/message-scheduler.cjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/config-file.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js"
|
|
21
|
+
"check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check server/composio-email.cjs && node --check server/attachments.cjs && node --check server/write-control.cjs && node --check server/secretary-store.cjs && node --check server/secretary-collector.cjs && node --check server/secretary-latest.cjs && node --check server/secretary-approvals.cjs && node --check server/secretary-rules.cjs && node --check server/secretary-shadow-gate.cjs && node --check server/secretary-vault-importer.cjs && node --check server/linkedin-archive-importer.cjs && node --check server/voice-profile.cjs && node --check server/campaign-store.cjs && node --check server/campaign-runner.cjs && node --check server/withdrawal-store.cjs && node --check server/post-scheduler.cjs && node --check server/message-scheduler.cjs && node --check scripts/import-linkedin-archive.mjs && node --check scripts/import-secretary-vault-snapshot.mjs && node --check scripts/rebuild-secretary-actors.mjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/config-file.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"mcp",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: secretary
|
|
3
|
+
description: Render Federico's stored inbound SignalDash triage when he asks for "latest", then record an exact yes or no for one displayed draft through SignalDash.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Secretary latest
|
|
7
|
+
|
|
8
|
+
When Federico asks `latest`, call `sd_secretary_latest`. This is a stored-data
|
|
9
|
+
read. Do not call LinkedIn, list chats, read threads, draft with a model, or use
|
|
10
|
+
any provider-backed tool as part of this flow.
|
|
11
|
+
|
|
12
|
+
Keep the most recent successful response's `ack_token` and nullable `next`
|
|
13
|
+
inside the current Claude session. On the next `latest` call:
|
|
14
|
+
|
|
15
|
+
- pass the prior `ack_token` as `ack_token`;
|
|
16
|
+
- when the prior `next` was non-null, pass it as `continuation` too;
|
|
17
|
+
- replace both saved values only after a successful response;
|
|
18
|
+
- after any error, retain the prior values and report the exact error.
|
|
19
|
+
|
|
20
|
+
Render exactly one Markdown table. Before it, state collection warnings from
|
|
21
|
+
`completeness` in one short sentence, or state `Collection complete` when the
|
|
22
|
+
array is empty. Use these columns:
|
|
23
|
+
|
|
24
|
+
| Priority | When | Who | Channel | Exact inbound | Disposition | Action | Draft |
|
|
25
|
+
|---|---|---|---|---|---|---|---|
|
|
26
|
+
|
|
27
|
+
Keep the server order. Copy exact inbound text and draft text byte for byte,
|
|
28
|
+
using `<br>` only to render embedded line breaks inside a table cell. Escape
|
|
29
|
+
literal pipe characters for Markdown without changing their meaning. Never
|
|
30
|
+
invent an actor name, summary, disposition, recipient, or draft.
|
|
31
|
+
|
|
32
|
+
Use these priority labels for returned categories:
|
|
33
|
+
|
|
34
|
+
- `decisions_federico_owes`: Decision
|
|
35
|
+
- `drafts_waiting_for_yes`: Yes or no
|
|
36
|
+
- `shadow_since_last_asked`: Shadow
|
|
37
|
+
- `auto_sent_since_last_asked`: Auto-sent
|
|
38
|
+
- `leads_routed_to_beatriz`: Beatriz
|
|
39
|
+
|
|
40
|
+
After all returned rows, add exactly one final table row for `ignored`, even
|
|
41
|
+
when its total is zero. Put `Ignored` in Priority, the aggregate total and
|
|
42
|
+
reason counts in Action, and `-` in the other cells. When there are no returned
|
|
43
|
+
rows, this ignored row is still the only table row.
|
|
44
|
+
|
|
45
|
+
For a draft, display `approval_view.text` in Draft. For a shadow row, display
|
|
46
|
+
`shadow_view.exact_text` in Draft when it is non-null, byte for byte. A shadow
|
|
47
|
+
view is review evidence only and never an approval surface. A missing approval view is
|
|
48
|
+
not approval-ready. Ask Federico for yes or no only when the exact immutable
|
|
49
|
+
approval view is present. The voice evidence comes from
|
|
50
|
+
`approval_view.draft_voice`: measured LinkedIn corpus, median 7 words,
|
|
51
|
+
lowercase openers, zero observed em dashes, German for German input and English
|
|
52
|
+
otherwise. Never replace that evidence with a generic style prompt.
|
|
53
|
+
|
|
54
|
+
On an explicit yes to one exact displayed draft, call
|
|
55
|
+
`sd_secretary_approve` once with only its `disposition_id`, `payload_hash`, and
|
|
56
|
+
`confirm:true`. Then call `li_send_message` once with only the returned
|
|
57
|
+
`secretary_receipt_id`. Never pass a chat, recipient, text, attachment,
|
|
58
|
+
watermark, or mark-read field beside the receipt. The server derives the exact
|
|
59
|
+
payload and runs the existing read, freshness, duplicate, budget, queue,
|
|
60
|
+
warning, and outcome guards. Report the exact refusal when any guard stops it.
|
|
61
|
+
|
|
62
|
+
On an explicit no, call `sd_secretary_reject` once with only the displayed
|
|
63
|
+
`disposition_id` and `confirm:true`. This records the rejection and sends
|
|
64
|
+
nothing. Discussion, an edit, or approval of another draft is neither yes nor
|
|
65
|
+
no for the displayed payload. An edit requires a new current disposition and
|
|
66
|
+
a new approval view.
|
|
@@ -51,7 +51,7 @@ in their terminal first, then continue.
|
|
|
51
51
|
npx -y @floomhq/signaldash@{{PACKAGE_VERSION}} <invite-code>
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
This logs in, copies the package-bundled
|
|
54
|
+
This logs in, copies the package-bundled SignalDash and Secretary skills into Claude Code, registers
|
|
55
55
|
the local MCP server, and prints browser links the human opens to connect
|
|
56
56
|
each account.
|
|
57
57
|
|
|
@@ -59,17 +59,19 @@ in their terminal first, then continue.
|
|
|
59
59
|
the provider account link. Hand the link to the user and wait for them to
|
|
60
60
|
complete Connect in their own browser.
|
|
61
61
|
|
|
62
|
-
2. The package writes the installed
|
|
62
|
+
2. The package writes the installed copies to the one client layout it supports
|
|
63
63
|
today:
|
|
64
64
|
|
|
65
65
|
- Claude Code: `~/.claude/skills/signaldash/SKILL.md`
|
|
66
|
+
- Claude Code: `~/.claude/skills/secretary/SKILL.md`
|
|
66
67
|
|
|
67
68
|
Other clients (Codex, a portable `.agents/skills/` layout, etc.) are not
|
|
68
69
|
written automatically. Copy this same reviewed file into that client's own
|
|
69
70
|
skill directory by hand and reload skill discovery there.
|
|
70
71
|
|
|
71
|
-
3. Confirm that the
|
|
72
|
-
`Before every send` section
|
|
72
|
+
3. Confirm that the SignalDash file begins with `name: signaldash` and contains the
|
|
73
|
+
`Before every send` section, and the Secretary file begins with `name: secretary`.
|
|
74
|
+
A partial copy is not installed.
|
|
73
75
|
|
|
74
76
|
4. Enable or reload skills according to the current agent. Start a new agent
|
|
75
77
|
session when the client discovers skills only at startup. After SignalDash
|
|
@@ -86,7 +88,8 @@ in their terminal first, then continue.
|
|
|
86
88
|
|
|
87
89
|
```text
|
|
88
90
|
Installed the SignalDash skill to /home/user/.claude/skills/signaldash/SKILL.md
|
|
89
|
-
|
|
91
|
+
Installed the Secretary skill to /home/user/.claude/skills/secretary/SKILL.md
|
|
92
|
+
Your agent now knows how to use SignalDash safely and render Secretary latest.
|
|
90
93
|
```
|
|
91
94
|
|
|
92
95
|
Persisting the reviewed, package-bundled skill is part of setup. Do not continue
|
|
@@ -359,9 +362,14 @@ Use the exact tool names and argument keys below. Limits are optional.
|
|
|
359
362
|
|
|
360
363
|
| Tool | Arguments | When to use it |
|
|
361
364
|
|---|---|---|
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
+
| `sd_version` | no arguments | Read the backend commit, dirty-at-start state, process start time, and package version captured when the serving process started. Use it to distinguish stale deployment from a code failure. |
|
|
366
|
+
| `sd_secretary_latest` | `limit` optional integer 1-100, default 50; `continuation` optional signed token; `ack_token` optional signed token from the previous successful response | Read the stored Secretary inbox with zero provider calls. Render its rows plus the ignored aggregate as one table. Preserve the prior tokens after an error; on the next successful call pass its acknowledgement and continuation exactly as returned. |
|
|
367
|
+
| `sd_secretary_approve` | `disposition_id` required; `payload_hash` required exact 64-character hash from latest; `confirm:true` required | Create one single-use, 15-minute receipt for the exact current stored draft. It derives the sender and executable payload from storage. |
|
|
368
|
+
| `sd_secretary_reject` | `disposition_id` required; `confirm:true` required | Reject one exact current stored draft. It sends nothing. |
|
|
369
|
+
| `li_list_chats` | `limit` integer 1-100, default 20; `cursor` optional; `search` optional string, max 200 characters; `max_scan` optional integer 1-500; `unread` optional boolean | Find recent LinkedIn chats, unread counts, and exact `chat_id` values. `unread:true` is forwarded to the provider as `unread=true`; SignalDash does not fetch a full chat page and filter it locally. Search accepts a unique stored display name, exact `public_id`, member id, or ordinary chat field. A stored name/public id is resolved to its verified member id before matching, so a provider row with `name:null` remains findable. Without an explicit `max_scan`, a zero-match search extends from 200 up to a hard 500-chat/five-page bound; explicit bounds remain exact. Read `scanned_chats`, `pages_fetched`, `scan_limit`, and `exhaustive` before concluding absence. The response also reports any resolved public/member ids. Any other key, including `text` and `member_id`, is refused with `unsupported_parameter` rather than accepted and ignored. |
|
|
370
|
+
| `li_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Read one resolved LinkedIn conversation before summarizing, drafting, or sending. The response includes `current_watermark`, the resolved `chat`, and `sender_name` on each message where the exact sender identity can be resolved. |
|
|
371
|
+
| `li_mark_read` | `chat_id` required | Explicitly clear one LinkedIn chat's unread state. Reading never clears unread automatically. |
|
|
372
|
+
| `li_send_message` | Manual path: `chat_id` and `text` required; `expected_watermark` optional exact 64-character watermark; `mark_read` optional boolean. Secretary path: `secretary_receipt_id` alone. | Send one approved LinkedIn reply after an immediate read of that exact chat. A Secretary receipt derives the exact stored chat and text and refuses every caller-supplied override. |
|
|
365
373
|
| `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. |
|
|
366
374
|
| `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. |
|
|
367
375
|
| `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. |
|
|
@@ -387,19 +395,23 @@ Use the exact tool names and argument keys below. Limits are optional.
|
|
|
387
395
|
| `sd_withdrawal_batch_status` | `withdrawal_batch_id` required | Inspect or monitor one sweep: done and remaining, the per-day pace and this sweep's own daily allowance, the stop reason, parse failures, the exact people the exclusions protected, and the `approval_url` to hand the human. This read also authorizes a later exact cancel. |
|
|
388
396
|
| `sd_withdrawal_batch_approve` | `withdrawal_batch_id` required; `confirm_token` required in the exact `sd-xxxx-xxxx-xxxx` form the human read off the approval page | Record the human's approval using the single-use code that authenticated page minted for them. An agent cannot mint, guess, or bypass that code. |
|
|
389
397
|
| `sd_withdrawal_batch_cancel` | `withdrawal_batch_id` required; `approval_view_hash` required string or null exactly as inspected; `confirm:true` required | Revoke the approval and stop every withdrawal that has not started. It cannot recall one already submitted, and it cannot restore the three-week re-invite block for people already withdrawn. |
|
|
390
|
-
| `wa_list_chats` | `limit` integer 1-100, default 20; `cursor` optional; `search` optional string, max 200 characters; `max_scan` optional integer 1-500, default 200, and never more than five provider pages | Find an existing WhatsApp conversation and exact `chat_id`. With `search` it filters on `name`, `subject`, `id`, `provider_id` and `attendee_public_identifier`, which carries the phone number, and it normalizes digits so `+49 151 67609512`, `004915167609512` and `4915167609512@s.whatsapp.net` all match. The provider CANNOT filter a chat list, so SignalDash scans and filters: read `search.exhaustive` before you conclude anything, `search.limit_reached` to know whether the matches were truncated by `limit`, and `search.searched_fields` for what was actually compared. `false` means "not among the `scanned_chats` scanned", never "does not exist" -- continue from the returned `cursor`. When `started_from_cursor` is true the scan began mid-list, so even `exhaustive` says nothing about the chats before that cursor.
|
|
391
|
-
| `wa_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Verify a WhatsApp contact and recent history before summarizing, drafting, or sending. |
|
|
398
|
+
| `wa_list_chats` | `limit` integer 1-100, default 20; `cursor` optional; `search` optional string, max 200 characters; `max_scan` optional integer 1-500, default 200, and never more than five provider pages | Find an existing WhatsApp conversation and exact `chat_id`. Direct chats expose a resolved contact in both `name` and `attendee_name` when available; groups retain their provider group `name` and return `attendee_name:null`. With `search` it filters on `name`, `subject`, `id`, `provider_id` and `attendee_public_identifier`, which carries the phone number, and it normalizes digits so `+49 151 67609512`, `004915167609512` and `4915167609512@s.whatsapp.net` all match. The provider CANNOT filter a chat list, so SignalDash scans and filters: read `search.exhaustive` before you conclude anything, `search.limit_reached` to know whether the matches were truncated by `limit`, and `search.searched_fields` for what was actually compared. `false` means "not among the `scanned_chats` scanned", never "does not exist" -- continue from the returned `cursor`. When `started_from_cursor` is true the scan began mid-list, so even `exhaustive` says nothing about the chats before that cursor. An unresolved 1:1 name remains null, so search the exact phone number instead of guessing. Any other key, including `text` and `member_id`, is refused with `unsupported_parameter`. |
|
|
399
|
+
| `wa_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Verify a WhatsApp contact and recent history before summarizing, drafting, or sending. The response includes `current_watermark` for an exact compare-and-swap send. |
|
|
400
|
+
| `wa_mark_read` | `chat_id` required | Explicitly clear one WhatsApp chat's unread state. Reading never clears unread automatically. |
|
|
392
401
|
| `wa_get_attachment` | `chat_id`, `message_id`, `attachment_id` all required, max 500 characters each | Download one attachment of one message in a chat this account owns. Read the chat first: the exact `message_id` and `attachment_id` come from `wa_read_messages`. Returns the stored path on the SignalDash host, mimetype, byte size and sha256. |
|
|
393
402
|
| `wa_transcribe_voice` | `chat_id`, `message_id`, `attachment_id` all required, max 500 characters each; `backend` optional, exactly `gemini` or `whisper` | Turn one WhatsApp voice note into text through SignalDash instead of fetching provider bytes yourself. Always read the returned `backend`: `gemini` is the accurate default, `whisper-small` is the weak local fallback and mangles German with English terms mixed in, and a fallback also carries `fallback_reason`. Non-audio attachments are refused with `415 not_audio`; an unknown backend with `400 unknown_backend`; a transcription that exceeds its time limit returns `504 transcription_timeout` with the stored audio path. |
|
|
394
|
-
| `wa_send_message` | `chat_id` required; `text` optional only when a file is attached, max 5000 characters; `attachments` optional array of up to 4 exact `{filename, content_type, content_base64}` files, at most 16 MiB per file and 16 MiB per message, types `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `application/pdf`, `text/csv`, `text/plain`, `application/json`, `application/zip`, xlsx | Send one approved reply, one approved file, or both, in an existing WhatsApp conversation after an immediate re-read. A file spends the same daily send budget and is recorded the same way as a text message; there is no separate attachment budget. A call carrying neither text nor an attachment is refused with `400 text_or_attachment_required`. Attachments are checked before anything is reserved, so a refusal costs no send: `400 unsupported_attachment_type`, `400 attachment_too_large`, `400 attachments_too_large`, `400 too_many_attachments`, `400 malformed_attachment_base64`, `400 invalid_attachment_filename`, and `413 request_too_large` when the whole body is too big to read. Nothing is ever truncated or dropped silently. The same caption with the same file is refused as `409 duplicate_send`; the same caption with a different file is a different message and goes through. If a send times out or the provider never confirms it, the message may still have been delivered: an identical retry is refused with `409 send_outcome_unknown`. Read the chat AGAIN, and ONLY if the message is genuinely absent, resend the identical payload with `confirm_resend:true`. The re-read is enforced, not advisory: a `confirm_resend` whose most recent read of that chat predates the failed attempt is refused with `428 reread_after_failed_send_required`, because a read taken before the attempt cannot show whether the message arrived.
|
|
403
|
+
| `wa_send_message` | `chat_id` required; `text` optional only when a file is attached, max 5000 characters; `attachments` optional array of up to 4 exact `{filename, content_type, content_base64}` files, at most 16 MiB per file and 16 MiB per message, types `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `application/pdf`, `text/csv`, `text/plain`, `application/json`, `application/zip`, xlsx; `expected_watermark` optional exact 64-character watermark; `mark_read` optional boolean, default false | Send one approved reply, one approved file, or both, in an existing WhatsApp conversation after an immediate re-read. Own outbound additions do not invalidate the read; inbound additions or mutations return `new_messages`, `changed_kind`, and `current_watermark`. Pass `expected_watermark` to bind the send to the exact reviewed state and `mark_read:true` only when the approved workflow also calls for clearing unread after the confirmed send. A file spends the same daily send budget and is recorded the same way as a text message; there is no separate attachment budget. A call carrying neither text nor an attachment is refused with `400 text_or_attachment_required`. Attachments are checked before anything is reserved, so a refusal costs no send: `400 unsupported_attachment_type`, `400 attachment_too_large`, `400 attachments_too_large`, `400 too_many_attachments`, `400 malformed_attachment_base64`, `400 invalid_attachment_filename`, and `413 request_too_large` when the whole body is too big to read. Nothing is ever truncated or dropped silently. The same caption with the same file is refused as `409 duplicate_send`; the same caption with a different file is a different message and goes through. If a send times out or the provider never confirms it, the message may still have been delivered: an identical retry is refused with `409 send_outcome_unknown`. Read the chat AGAIN, and ONLY if the message is genuinely absent, resend the identical payload with `confirm_resend:true`. The re-read is enforced, not advisory: a `confirm_resend` whose most recent read of that chat predates the failed attempt is refused with `428 reread_after_failed_send_required`, because a read taken before the attempt cannot show whether the message arrived. A re-read failure remains `502 thread_preflight_unavailable`, and a legacy proof with no watermark remains `428 read_before_send_required`; neither costs send budget. LinkedIn messages carry text only. |
|
|
395
404
|
| `wa_delete_message` | `chat_id`, `message_id` both required, max 500 characters each | Retract one message THIS account sent, in a chat this account owns. The exact `message_id` comes from `wa_read_messages`. Irreversible and never retried: someone else's message is refused with `403 message_not_own`, a message outside this chat with `403 message_forbidden`, and a delete already recorded for this exact chat and message with `409 duplicate_delete`. Deletes spend their own daily budget, so `429 rate_limit_exceeded` here never means you are out of sends. WhatsApp applies its own time and role limits to deleting for everyone and can answer successfully without removing anything, so read the chat again to confirm the message is gone. |
|
|
396
405
|
| `wa_delete_messages` | `messages` required array of 1-200 exact `{chat_id, message_id}` objects | Retract several messages this account sent. Same ownership, budget and audit path as `wa_delete_message`, executed strictly one at a time with a pause between them, never in parallel. Always read the per-entry `ok`, `code` and `error`: a partial result is normal. Entries the batch never reached before its time limit come back with `skipped:true` and `code:batch_deadline`, and were not attempted; resend exactly those to resume. |
|
|
397
406
|
| `email_list` | `limit` integer 1-100, default 20; `cursor` optional, max 4096 | List the newest message in each recent email thread and obtain `thread_id`. The response carries a `cursor`; pass it back to read the next page, and omit it for the first. |
|
|
398
407
|
| `email_read` | `thread_id` required; `limit` 1-100, default 30 | Read an email thread and authorize its exact participant addresses for a later send. Everyone on `cc` counts as a participant, so a read authorizes them too. The newest messages are returned, not the oldest, so the people being replied to are always in the window. |
|
|
399
408
|
| `email_send` | `to` required as an array of exactly one valid address; `subject` required, max 998, single line; `body` required, max 5000; `thread_id` optional, max 500 | Send one approved email to a participant in a recently read existing thread. Pass the `thread_id` you read to reply inside that thread; omit it only when starting a new one. A blank or oversized `thread_id` is refused with `400 invalid_request` rather than quietly starting a new thread beside the original. The same subject and body to the same person in the same thread is refused as `409 duplicate_send`; the same words in a different thread are a different message and go through. A subject containing a line break is refused with `400 invalid_request`, because a subject is one header line; a body with line breaks is normal and sends. If a send leaves this host and the provider never answers, the retry is refused as `409 send_outcome_unknown`: read the thread again, and only if the email is genuinely absent resend with `confirm_resend: true`. |
|
|
400
|
-
| `li_my_posts` | `limit` default 10, max 50; `member_id` optional | Find the user's latest posts and
|
|
401
|
-
| `li_post_reactions` | `post_id` required; `limit` default 50, max 100 |
|
|
402
|
-
| `li_post_comments` | `post_id` required; `limit` default 50, max 100 | Read comments
|
|
409
|
+
| `li_my_posts` | `limit` default 10, max 50; `member_id` optional | Find the user's latest posts and exact `social_id` values. Omit `member_id` to use the connected user's own ID. Pass `social_id`, not a different numeric `id`, to the engagement tools. |
|
|
410
|
+
| `li_post_reactions` | `post_id` required; use the exact `social_id` from `li_my_posts`; `limit` default 50, max 100; `cursor` optional | Read one bounded page of reactors. Always inspect `completeness.state`, `total`, and `next_cursor`; continue with the cursor while state is `incomplete`. Numeric ids are resolved against the user's own recent posts when possible, including ugcPost-backed multi-image posts. The provider total is reported when available and is never guessed from `li_my_posts`. A reaction does not authorize outreach. |
|
|
411
|
+
| `li_post_comments` | `post_id` required; `comment_id` optional exact parent id; use the exact `social_id` from `li_my_posts`; `limit` default 50, max 100; `cursor` optional | Read one bounded page of comments, or pass `comment_id` to read replies to that exact parent. Always inspect `completeness.state`, `total`, and `next_cursor`; continue with the cursor while state is `incomplete`. Numeric ids are resolved against the user's own recent posts when possible. The provider total is reported when available and is never guessed from `li_my_posts`. |
|
|
412
|
+
| `li_reply_to_comment` | Manual path: `post_id`, `parent_comment_id`, `trigger_comment_id`, and `text` required; `expected_watermark` optional exact 64-character watermark. Secretary path: `secretary_receipt_id` alone. | Reply once to one exact inbound comment on this sender's own post after `li_post_comments`. SignalDash re-proves post ownership, the unchanged trigger, its author and parent, no own duplicate, sender generation, budget, and provider health immediately before writing. A 2xx is not success until readback finds exactly one matching own reply. |
|
|
413
|
+
| `li_delete_message` | `chat_id`, `message_id`, and `confirm:true` required | Remediate one exact own LinkedIn message within 60 minutes of sending. SignalDash proves account, exact chat, own authorship, timestamp eligibility, a separate remediation budget, and post-delete absence. This cannot undo delivery, reading, or notifications and never relaxes a send gate. |
|
|
414
|
+
| `li_delete_comment` | `post_id`, `comment_id`, and `confirm:true` required | Currently unavailable: the fixture-tested Unipile v2 wrapper is held at database capability state `untested` until live compatibility is proved against Federico's own removable comment. When enabled it proves own comment identity and readback. Deletion is remediation, not rollback. |
|
|
403
415
|
| `li_draft_post` | `text` required, max 3000; `publish` optional; `scheduled_at` optional offset-qualified ISO date-time; `mentions` optional array of up to 20 exact `{name,profile_id}` objects; `attachments` optional array of up to 4 exact `{filename,content_type,content_base64}` images; `first_comment` optional, max 1250 | Create a server-confirmed draft, publish now, or persist an exact future LinkedIn post and its approved first comment. A schedule is stored only with `publish:true` after approval of the exact text, time, mentions, images, and comment. |
|
|
404
416
|
| `li_set_scheduled_post_first_comment` | `id` required UUID; `first_comment` required, max 1250; `confirm:true` required | Attach one exact approved first comment to a scheduled post. SignalDash publishes it through the same connected account after the post and never republishes the post if the comment fails. |
|
|
405
417
|
| `li_scheduled_posts` | no arguments | List only this authenticated user's scheduled LinkedIn posts and their durable states. |
|
|
@@ -411,6 +423,8 @@ Use the exact tool names and argument keys below. Limits are optional.
|
|
|
411
423
|
Representative calls:
|
|
412
424
|
|
|
413
425
|
```text
|
|
426
|
+
sd_version({})
|
|
427
|
+
sd_secretary_latest({})
|
|
414
428
|
li_list_chats({"limit":20})
|
|
415
429
|
li_read_messages({"chat_id":"chat_li_7f3a","limit":20})
|
|
416
430
|
li_send_message({"chat_id":"chat_li_7f3a","text":"Yes. I’ll send it this afternoon."})
|
|
@@ -452,8 +466,11 @@ email_list({"limit":20})
|
|
|
452
466
|
email_read({"thread_id":"thread_email_c402","limit":30})
|
|
453
467
|
email_send({"to":["amina@example.com"],"subject":"Re: Case study","body":"Hi Amina,\n\nHere is the case study.","thread_id":"thread_email_c402"})
|
|
454
468
|
li_my_posts({"limit":5})
|
|
455
|
-
li_post_reactions({"post_id":"
|
|
456
|
-
li_post_comments({"post_id":"
|
|
469
|
+
li_post_reactions({"post_id":"urn:li:activity:8821","limit":50})
|
|
470
|
+
li_post_comments({"post_id":"urn:li:activity:8821","limit":50})
|
|
471
|
+
li_reply_to_comment({"post_id":"urn:li:activity:8821","parent_comment_id":"comment-root","trigger_comment_id":"comment-root","text":"Thanks!","expected_watermark":"exact-64-character-watermark-from-the-read"})
|
|
472
|
+
li_delete_message({"chat_id":"chat_li_7f3a","message_id":"msg_li_5c71","confirm":true})
|
|
473
|
+
li_delete_comment({"post_id":"exact-v2-post-id","comment_id":"exact-own-comment-id","confirm":true})
|
|
457
474
|
li_draft_post({"text":"Most agents need better context, not more autonomy."})
|
|
458
475
|
li_set_scheduled_post_first_comment({"id":"00000000-0000-4000-8000-000000000000","first_comment":"https://github.com/xai-org/x-algorithm","confirm":true})
|
|
459
476
|
li_scheduled_posts({})
|
|
@@ -933,14 +950,17 @@ against the returned headline.
|
|
|
933
950
|
|
|
934
951
|
Each result contains only:
|
|
935
952
|
|
|
936
|
-
- `name`, `headline`, `public_id`, `profile_url`, and
|
|
937
|
-
|
|
938
|
-
|
|
953
|
+
- `name`, `headline`, `public_id`, `member_id`, `profile_url`, and
|
|
954
|
+
`connected_at`; `member_id` is null only for a legacy row that has not yet
|
|
955
|
+
been refreshed or verified by chat search;
|
|
956
|
+
- `already_in_contact`, based on an exact known chat or recorded inbound,
|
|
957
|
+
outbound, reply, or invitation history;
|
|
939
958
|
- the joined `contact_state`, `campaign_eligible`, and `blocking_reasons`.
|
|
940
959
|
|
|
941
|
-
The join checks only the row's exact LinkedIn public identifier
|
|
942
|
-
profile URL
|
|
943
|
-
|
|
960
|
+
The join checks only the row's exact LinkedIn member id, public identifier,
|
|
961
|
+
canonical profile URL, and verified known-chat mapping for this SignalDash
|
|
962
|
+
user. It does not infer a person, merge aliases, or inspect another tenant.
|
|
963
|
+
Search results are planning evidence, not permission
|
|
944
964
|
to message or invite anyone. A later action still requires its exact
|
|
945
965
|
one-object read/preview, human review, approval, fresh preflight, and server
|
|
946
966
|
guards. The executable invitation-batch design does not accept a search result
|
|
@@ -1084,19 +1104,20 @@ message you cannot yet schedule.
|
|
|
1084
1104
|
|
|
1085
1105
|
### 409 `thread_changed`
|
|
1086
1106
|
|
|
1087
|
-
Meaning: the
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
written against.
|
|
1107
|
+
Meaning: the counterparty added a message, an existing item changed or was
|
|
1108
|
+
deleted, or an explicit `expected_watermark` no longer matches. Both send tools
|
|
1109
|
+
return `new_messages`, `changed_kind`, and `current_watermark`. Additions made
|
|
1110
|
+
only by this account are classified as `own_outbound` and do not invalidate a
|
|
1111
|
+
send unless the caller supplied an explicit watermark that no longer matches.
|
|
1093
1112
|
|
|
1094
1113
|
Comply:
|
|
1095
1114
|
|
|
1096
|
-
1.
|
|
1097
|
-
2.
|
|
1098
|
-
|
|
1099
|
-
|
|
1115
|
+
1. Inspect `new_messages` and `changed_kind`.
|
|
1116
|
+
2. Revise the draft against the new context and obtain approval again when the
|
|
1117
|
+
change affects the approved text.
|
|
1118
|
+
3. Pass the returned `current_watermark` as `expected_watermark` to bind the
|
|
1119
|
+
next attempt to exactly that reviewed state.
|
|
1120
|
+
4. Send once, promptly. A second change returns another observable 409.
|
|
1100
1121
|
|
|
1101
1122
|
A `502 thread_preflight_unavailable` on a send means that re-read itself failed,
|
|
1102
1123
|
so whether the thread changed is unknown and nothing was sent. Retry the send
|
|
@@ -1106,11 +1127,10 @@ same fact: no answer about whether the conversation moved. In that case a `428
|
|
|
1106
1127
|
read_before_send_required` on the next attempt is expected too, because a read
|
|
1107
1128
|
of an unrecognised shape records no authorization.
|
|
1108
1129
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
as intended, then send the second. If you meant one message, send one message.
|
|
1130
|
+
A confirmed send advances the stored proof. Distinct consecutive messages can
|
|
1131
|
+
therefore pass without a fresh read when the only movement is this account's
|
|
1132
|
+
own outbound. Every send still re-reads the provider thread, and a counterparty
|
|
1133
|
+
message or mutation still refuses before budget or provider write.
|
|
1114
1134
|
|
|
1115
1135
|
### 409 `duplicate_send`
|
|
1116
1136
|
|
|
@@ -1299,16 +1319,22 @@ User: "Who engaged with my last LinkedIn post, and what should I do?"
|
|
|
1299
1319
|
```
|
|
1300
1320
|
|
|
1301
1321
|
2. Select the newest post by its provider timestamp, not by list position when
|
|
1302
|
-
timestamps disagree. Record its exact `
|
|
1303
|
-
3. Call
|
|
1322
|
+
timestamps disagree. Record its exact `social_id`.
|
|
1323
|
+
3. Call with the exact `social_id` from that post, not a different numeric `id`:
|
|
1304
1324
|
|
|
1305
1325
|
```text
|
|
1306
|
-
li_post_reactions({"post_id":"
|
|
1307
|
-
li_post_comments({"post_id":"
|
|
1326
|
+
li_post_reactions({"post_id":"urn:li:activity:8821","limit":50})
|
|
1327
|
+
li_post_comments({"post_id":"urn:li:activity:8821","limit":50})
|
|
1308
1328
|
```
|
|
1309
1329
|
|
|
1310
1330
|
4. Group the result:
|
|
1311
1331
|
|
|
1332
|
+
Before grouping, inspect `completeness` on both responses. While its state
|
|
1333
|
+
is `incomplete`, repeat the call with `cursor: completeness.next_cursor`.
|
|
1334
|
+
A state of `unknown` is not a complete sweep; report that the provider did
|
|
1335
|
+
not prove completeness. Never substitute `li_my_posts.reaction_counter` as
|
|
1336
|
+
a fabricated result total.
|
|
1337
|
+
|
|
1312
1338
|
- Direct questions in comments: respond publicly or draft a reply.
|
|
1313
1339
|
- Existing contacts with substantive engagement: warm follow-up candidate,
|
|
1314
1340
|
but no message without thread verification and approval.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: signaldash-safe-usage
|
|
3
|
-
description: Safely search stored LinkedIn connections, inspect exact contact state and action budgets,
|
|
3
|
+
description: Safely search stored LinkedIn connections, inspect exact contact state and action budgets, approve or reject exact Secretary drafts, read, mark read, reply to comments, delete exact own content, and send LinkedIn, WhatsApp, or email messages through the SignalDash MCP without triggering account restrictions or contacting the wrong person. Use whenever an agent uses a SignalDash read, send, invitation, comment-reply, or delete tool.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Use SignalDash safely
|
|
@@ -23,6 +23,15 @@ Treat LinkedIn, WhatsApp, and email as human accounts, not bulk messaging APIs.
|
|
|
23
23
|
Never infer a recipient from a partial name. Never send blind. Never retry a
|
|
24
24
|
send after an ambiguous timeout without first reading the thread.
|
|
25
25
|
|
|
26
|
+
Reading is non-mutating. Clear unread only through an explicit `li_mark_read`
|
|
27
|
+
or `wa_mark_read` call, or an approved send with `mark_read:true`.
|
|
28
|
+
|
|
29
|
+
For a Secretary draft, approve only the exact `disposition_id` and
|
|
30
|
+
`payload_hash` returned by `sd_secretary_latest`. Pass the resulting
|
|
31
|
+
`secretary_receipt_id` alone to `li_send_message`. Never add or edit a chat,
|
|
32
|
+
recipient, text, attachment, watermark, or mark-read field beside the receipt.
|
|
33
|
+
On no, call `sd_secretary_reject` for that exact disposition and send nothing.
|
|
34
|
+
|
|
26
35
|
## Contact-state limits
|
|
27
36
|
|
|
28
37
|
- `sd_contact_state` uses exact user-and-channel identifiers only. It does not
|
|
@@ -113,6 +122,15 @@ send after an ambiguous timeout without first reading the thread.
|
|
|
113
122
|
cached records.
|
|
114
123
|
- Stop all LinkedIn activity on any checkpoint, warning, restriction, unusual
|
|
115
124
|
verification prompt, HTTP 403, or HTTP 429.
|
|
125
|
+
- Before `li_reply_to_comment`, read the exact own post comments, preserve the
|
|
126
|
+
exact parent, trigger and watermark, show the full reply text, and obtain
|
|
127
|
+
approval. The server readback, not the provider 2xx alone, proves success.
|
|
128
|
+
- Before `li_delete_message` or any future enabled `li_delete_comment`, read
|
|
129
|
+
and show the exact own object and obtain explicit approval. Deletion is
|
|
130
|
+
remediation after exposure, not rollback. It cannot undo a notification or
|
|
131
|
+
a recipient having read the content, and it never weakens any send gate.
|
|
132
|
+
- Treat `li_delete_comment` as unavailable while its capability reports
|
|
133
|
+
`untested`. Never change that flag merely to complete a task.
|
|
116
134
|
|
|
117
135
|
LinkedIn detects behavior, not only API request frequency. Repeated copy,
|
|
118
136
|
bursting sends, high profile-view volume, many ignored invitations, concurrent
|