@floomhq/signaldash 0.13.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: signaldash
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.
3
+ description: Operate the user's SignalDash connection to LinkedIn, WhatsApp, and email. Use this skill whenever the user says or implies "check my LinkedIn", "who messaged me", "triage my inbox", "read the thread with X", "reply to X", "send this message", "send a connection request", "create an invitation batch", "check invitations", "accept this invitation", "auto-accept my invitations", "check auto-accept status", "withdraw this invitation", "search my LinkedIn connections", "find people in my network", "find people beyond my network", "suppress this contact", "check contact state", "check my action budget", "check WhatsApp", "check my email", "draft a LinkedIn post", "who engaged with my post", "who liked or commented", or "export my LinkedIn connections". Covers durable installation, invite-based setup, local-first connection search, capped paid discovery, immutable human-approved invitation batches, opt-in rate-capped invitation auto-accept, exact contact state and suppression, authoritative LinkedIn action-budget status, safe reading, drafting, approved sends, exact invitation actions, post engagement analysis, and paced connection export.
4
4
  ---
5
5
 
6
6
  # Operate SignalDash
@@ -91,14 +91,41 @@ Typical trigger phrases include:
91
91
  - "Check my email", "summarize the thread", "draft a response", or "send the
92
92
  approved email."
93
93
  - "Draft a post", "turn this into a LinkedIn post", or "publish this post."
94
+ - "Send a connection request to this exact person", "check my invitations",
95
+ "accept this invitation", or "withdraw this pending invitation."
96
+ - "How many LinkedIn actions remain?", "check my action budget", or "can a
97
+ campaign act today?"
98
+ - "Auto-accept my LinkedIn invitations", "turn invitation auto-accept off", or
99
+ "did auto-accept hit any errors?" Use the persistent per-user setting and
100
+ status tools; it is off until explicitly enabled.
101
+ - "Search my LinkedIn connections", "find founders in my network", or "who at
102
+ Acme am I connected to?" Use the local connection snapshot before any
103
+ external or paid discovery.
104
+ - "Find platform engineers beyond my network" or "discover people at Acme in
105
+ Berlin." Use `li_discover_people` only after the completed local snapshot
106
+ returns no matches. Preview the exact paid request and cost before confirming
107
+ one capped provider page.
108
+ - "Schedule this message", "follow up automatically", or "send these as a
109
+ sequence." Explain the narrow campaign time boundary below; do not invent a
110
+ queue or claim those unsupported actions were scheduled.
111
+ - "Create a LinkedIn invitation batch for these exact profiles." Structure the
112
+ human's request into exact URLs, reasons, and notes, create the immutable
113
+ preview, and relay its human review path. Never approve through MCP.
94
114
  - "Who engaged with my last post?", "who liked it?", "what did people comment?",
95
115
  or "which warm signals need action?"
96
116
  - "Export my connections", "download my LinkedIn network", or "make me a
97
117
  connections CSV."
98
118
 
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.
119
+ Do not use SignalDash for general public LinkedIn research, full-profile
120
+ enrichment, email finding, unreviewed bulk outreach, unapproved automatic
121
+ invitation processing, or a new email to someone with no existing thread.
122
+ `li_discover_people` is the narrow exception for one capped page of public
123
+ search cards after own-network search. Its results are planning evidence only.
124
+ Invitation tools act on one exact provider member or invitation at a time and
125
+ never authorize a list-wide loop. The only automatic incoming-invitation
126
+ exception is the explicitly enabled, filterable, rate-capped auto-accept worker.
127
+ The only multi-target outbound exception is an immutable 1–10-target invitation
128
+ batch with separate exact browser approval and server-controlled execution.
102
129
 
103
130
  ## The operating model
104
131
 
@@ -106,8 +133,10 @@ There are two surfaces:
106
133
 
107
134
  1. The CLI handles login, account connection, status, skill installation, MCP
108
135
  startup, logout, and the paced LinkedIn connections export.
109
- 2. MCP tools handle account reads, message sends, email, LinkedIn posts, and
110
- post engagement.
136
+ 2. MCP tools handle account reads, local connection search, capped paid
137
+ discovery, message sends, immutable invitation-batch create/inspect/cancel,
138
+ exact LinkedIn invitation actions, email, LinkedIn posts, and post
139
+ engagement.
111
140
 
112
141
  The CLI command used by the MCP registration is:
113
142
 
@@ -257,23 +286,30 @@ Use this sequence unless the request is read-only and ends before approval:
257
286
 
258
287
  1. **Confirm connection.** Run `npx -y @floomhq/signaldash status` when channel
259
288
  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.
289
+ 2. **Choose the narrowest list or preview tool.** List recent chats, email
290
+ threads, invitation inboxes, or the user's recent posts. Preview an outbound
291
+ invitation through `li_send_invitation` with `confirm` omitted. Use a modest
292
+ limit.
293
+ 3. **Resolve the exact object.** Match the full chat name, thread, invitation,
294
+ provider member, or post.
263
295
  When names collide or identity is unclear, show the candidates and ask the
264
296
  user. Never infer from a partial name.
265
297
  4. **Read before interpreting.** Read enough recent history to understand the
266
298
  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.
299
+ For message sends, read at least 10 recent items and use the exact chat or
300
+ thread. For invitation maintenance, list the exact current invitation. For
301
+ a new invitation, inspect the server-confirmed exact target and note preview.
268
302
  5. **Return findings or draft.** Summarize concrete facts. Separate suggested
269
303
  replies from messages already sent.
270
304
  6. **Obtain explicit approval.** Before any message or public post, show the
271
305
  exact channel, recipient, and complete text. For email include subject and
272
306
  body. Discussion, editing, "looks good", or approval of a different draft is
273
307
  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.
308
+ 7. **Re-read immediately before acting.** Re-read the exact thread or current
309
+ invitation list to catch a human action, reply, duplicate, acceptance, or
310
+ withdrawal that appeared after drafting. New invitation sends repeat the
311
+ exact previewed payload with `confirm:true`; the server rechecks relationship
312
+ and pending invitation state.
277
313
  8. **Act once.** Send one approved message or publish one approved post. Never
278
314
  parallelize sends and never loop over recipients.
279
315
  9. **Verify.** Read the exact thread again after a successful send. Confirm the
@@ -308,6 +344,21 @@ Use the exact tool names and argument keys below. Limits are optional.
308
344
  | `li_list_chats` | `limit` integer 1-100, default 20 | Find recent LinkedIn chats, unread counts, and exact `chat_id` values without opening profiles. |
309
345
  | `li_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Read one resolved LinkedIn conversation before summarizing, drafting, or sending. |
310
346
  | `li_send_message` | `chat_id` required; `text` required, max 5000 characters | Send one approved LinkedIn reply after an immediate read of that exact chat. |
347
+ | `li_send_invitation` | `provider_id` required; `note` optional, max 300 exact characters; `confirm` optional, default false | First preview one exact target and note. The server verifies relationship, both invitation directions, and absence of an existing one-to-one chat. After exact approval, repeat the identical call with `confirm:true`; jitter completes before the final preflight and action reservation. |
348
+ | `li_invitations_received` | `limit` integer 1-100, default 50; `cursor` optional | List one bounded page of received invitations. This read authorizes only the exact returned invitation IDs for a later accept. |
349
+ | `li_accept_invitation` | `invitation_id` required; `confirm:true` required | Accept one exact currently pending received invitation after a fresh `li_invitations_received` read and approval. |
350
+ | `li_invitations_sent` | `limit` integer 1-100, default 50; `cursor` optional | List one bounded page of sent invitations. This read authorizes only the exact returned invitation IDs for a later withdrawal. |
351
+ | `li_withdraw_invitation` | `invitation_id` required; `confirm:true` required | Withdraw one exact currently pending sent invitation after a fresh `li_invitations_sent` read and approval. |
352
+ | `sd_contact_state` | `channel` required (`linkedin`, `whatsapp`, or `email`); `identifiers` required array of 1-8 exact `{kind,value}` objects; `action` optional (`get` default or `suppress`); suppression also requires an allowed `reason` and `confirm:true` | Inspect exact tenant/channel-scoped contact history or add a protective suppression. It never infers that identifiers on different channels belong to one person. |
353
+ | `sd_budget_status` | no arguments | Read the current sender binding, daily total/manual/campaign/unknown attempts, total and combined-campaign capacity, weekly invitation usage, lock state, and UTC resets from the authoritative server ledger. |
354
+ | `sd_settings_get` | no arguments | Read the authenticated user's persistent SignalDash settings. Auto-accept is the first supported setting and is false for every existing user until explicitly changed. |
355
+ | `sd_settings_set` | `auto_accept_linkedin` required boolean; `auto_accept_linkedin_filters` optional object with `public_identifiers` and `description_keywords`; `confirm:true` required | Update the general per-user settings surface after exact human approval. This changes no future or unknown setting implicitly. |
356
+ | `sd_auto_accept_status` | no arguments | Inspect whether auto-accept is enabled, its exact filters, accepted today and this week, failed attempts today, dedicated and shared capacity, repeated-error stop state, and sanitized unparseable invitation records. |
357
+ | `li_search_connections` | `query` required string, max 200; `filters` optional object with `company`, `headline_keyword`, `connected_after`, and `connected_before`; `limit` integer 1-100, default 20 | Search only the authenticated user's stored LinkedIn connection snapshot and join exact local contact state. This makes no LinkedIn, Unipile, HarvestAPI, or other paid discovery call. |
358
+ | `li_discover_people` | `query` required role/title string, max 200; `filters` optional object with `company` and `location`; `limit` integer 1-10, default 10; `confirm` optional, default false | After a completed local snapshot returns no matches, preview one paid HarvestAPI profile-search page. Obtain approval for the exact query and maximum reserved cost, then repeat with `confirm:true`. |
359
+ | `li_create_invitation_batch` | `source_label` required exact string, max 120 code points; `time_zone` required IANA timezone; `targets` required array of 1-10 exact `{profile_url,inclusion_reason,note?}` objects; reason max 240 and note max 200 Unicode code points | Create one immutable durable preview from canonical LinkedIn Classic `/in/` URLs. The agent structures an explicit human request; the server never generates targets or text. |
360
+ | `li_get_invitation_batch` | `batch_id` required | Inspect every stored target, exact note, exclusion, hash, timing, capacity, and result. Use the returned browser review path for human approval. This read also authorizes a later exact cancel. |
361
+ | `li_cancel_invitation_batch` | `batch_id` required; `approval_view_hash` required string or null exactly as inspected; `confirm:true` required | Permanently cancel unstarted targets in one freshly inspected batch. It cannot recall an executing invitation, and restarting requires a new preview and approval. |
311
362
  | `wa_list_chats` | `limit` integer 1-100, default 20 | Find an existing WhatsApp conversation and exact `chat_id`. |
312
363
  | `wa_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Verify a WhatsApp contact and recent history before summarizing, drafting, or sending. |
313
364
  | `wa_send_message` | `chat_id` required; `text` required, max 5000 characters | Send one approved reply in an existing WhatsApp conversation after an immediate re-read. |
@@ -325,6 +376,24 @@ Representative calls:
325
376
  li_list_chats({"limit":20})
326
377
  li_read_messages({"chat_id":"chat_li_7f3a","limit":20})
327
378
  li_send_message({"chat_id":"chat_li_7f3a","text":"Yes. I’ll send it this afternoon."})
379
+ li_send_invitation({"provider_id":"ACoAAExactMember","note":"Hi Amina, I enjoyed your post on agent safety."})
380
+ li_send_invitation({"provider_id":"ACoAAExactMember","note":"Hi Amina, I enjoyed your post on agent safety.","confirm":true})
381
+ li_invitations_received({"limit":20})
382
+ li_accept_invitation({"invitation_id":"invite_received_42","confirm":true})
383
+ li_invitations_sent({"limit":20})
384
+ li_withdraw_invitation({"invitation_id":"invite_sent_91","confirm":true})
385
+ sd_contact_state({"channel":"linkedin","identifiers":[{"kind":"provider_id","value":"ACoAAExactMember"}]})
386
+ sd_contact_state({"channel":"email","identifiers":[{"kind":"email","value":"amina@example.com"}],"action":"suppress","reason":"opt_out","confirm":true})
387
+ sd_budget_status({})
388
+ sd_settings_get({})
389
+ sd_settings_set({"auto_accept_linkedin":true,"auto_accept_linkedin_filters":{"public_identifiers":["amina-rahman"],"description_keywords":["Founder"]},"confirm":true})
390
+ sd_auto_accept_status({})
391
+ li_search_connections({"query":"founder agents","filters":{"company":"Acme","headline_keyword":"Founder","connected_after":"2025-01-01","connected_before":"2026-01-01"},"limit":20})
392
+ li_discover_people({"query":"Platform Engineer","filters":{"company":"Acme","location":"Berlin"},"limit":5})
393
+ li_discover_people({"query":"Platform Engineer","filters":{"company":"Acme","location":"Berlin"},"limit":5,"confirm":true})
394
+ li_create_invitation_batch({"source_label":"Approved Berlin founder shortlist","time_zone":"Europe/Berlin","targets":[{"profile_url":"https://www.linkedin.com/in/amina-rahman/","inclusion_reason":"Named by the user for this exact batch","note":"Hi Amina, I enjoyed your post on agent safety."}]})
395
+ li_get_invitation_batch({"batch_id":"00000000-0000-4000-8000-000000000000"})
396
+ li_cancel_invitation_batch({"batch_id":"00000000-0000-4000-8000-000000000000","approval_view_hash":"exact-64-character-hash-from-the-read","confirm":true})
328
397
  wa_list_chats({"limit":20})
329
398
  wa_read_messages({"chat_id":"chat_wa_91b2","limit":20})
330
399
  wa_send_message({"chat_id":"chat_wa_91b2","text":"16:30 works. See you then."})
@@ -354,10 +423,267 @@ Publishing is public and irreversible. Never interpret "draft a post" as
354
423
  permission to publish. `email_send` cannot start a cold thread and must never be
355
424
  looped over recipients.
356
425
 
426
+ ### Exact contact state and suppression
427
+
428
+ `sd_contact_state` is local and account-scoped. It makes no Unipile, LinkedIn,
429
+ WhatsApp, or email provider request. Exact identifiers are keyed separately by
430
+ user and channel, and the response does not echo their values.
431
+
432
+ Allowed identifier kinds:
433
+
434
+ - LinkedIn: `provider_id`, `public_identifier`, canonical `profile_url`,
435
+ `member_urn`, or `chat_id`.
436
+ - WhatsApp: `chat_id` or an E.164 `phone`.
437
+ - Email: normalized `email`.
438
+
439
+ One fresh provider response can supply several exact identifiers for the same
440
+ channel. Pass those together to get the conservative union of their state.
441
+ Never combine identifiers based on a name, employer, headline, similar profile,
442
+ phone guess, or email guess. SignalDash does not infer cross-channel identity
443
+ and does not claim person-wide suppression.
444
+
445
+ Use `action:"get"` before outreach planning. `campaign_eligible:false` and
446
+ `blocking_reasons` report known replies, earlier outbound touches,
447
+ already-connected state, or suppression. Automated campaign actions are
448
+ rejected after a known reply or earlier touch. Manual approved replies remain
449
+ available unless an exact identifier is suppressed.
450
+
451
+ Use `action:"suppress"` only after the human explicitly asks to suppress the
452
+ exact identifier. Show the channel, identifier kind, and reason, then repeat
453
+ with `confirm:true`. Allowed reasons are `opt_out`, `not_interested`,
454
+ `provider_block`, `manual`, and `legal`. MCP cannot clear a suppression.
455
+
456
+ ### LinkedIn action budget status
457
+
458
+ `sd_budget_status` is read-only and account-scoped. Use it before planning
459
+ manual LinkedIn work or any campaign action. It reports attempts that
460
+ SignalDash has committed in its authoritative SQLite ledger:
461
+
462
+ - the total daily cap (`H`);
463
+ - the campaign-excluded portion (`R = min(4, H)`);
464
+ - the combined ceiling across all campaigns (`C = H - R`);
465
+ - total, manual, campaign, and unknown attempts for the UTC policy day;
466
+ - raw total and combined-campaign capacity remaining;
467
+ - actions available now after connection and sender-lock state;
468
+ - weekly LinkedIn invitation usage and its Monday 00:00 UTC reset.
469
+
470
+ `R` is capacity campaigns cannot consume. It is not a reserve or guaranteed
471
+ manual allowance: manual activity can consume the total cap first, and a later
472
+ manual action can still be rejected at `H`. Campaign capacity is shared across
473
+ all campaigns and is additionally bounded by the total remaining capacity.
474
+
475
+ The result counts SignalDash-recorded attempts only. Native LinkedIn activity
476
+ is not fully counted, so never describe the result as a LinkedIn-safe
477
+ allowance. A disconnected, unverified, or locked sender reports zero safe
478
+ actions even when raw counter capacity remains. It reports the durable pacing
479
+ time and work window after a campaign action has established them. The
480
+ scheduler, persisted tenant-breaker, active-batch, and target-count fields
481
+ reflect the current invitation-batch runtime.
482
+
483
+ ### Opt-in invitation auto-accept
484
+
485
+ `sd_settings_get` is the single read surface for persistent SignalDash
486
+ settings. `sd_settings_set` changes persistent behavior for only the
487
+ authenticated SignalDash user. Auto-accept is the first supported setting;
488
+ every existing and newly created user is disabled by default. Show the exact
489
+ setting and filters, obtain explicit human approval, then call the setter once
490
+ with `confirm:true`. Disabling also uses `confirm:true`. Never infer changes to
491
+ future or unknown settings.
492
+
493
+ With no filters, every parseable pending LinkedIn invitation is eligible. The
494
+ optional `public_identifiers` list is an exact lowercase allowlist. Optional
495
+ `description_keywords` are case- and accent-insensitive substring matches
496
+ against the provider's inviter description. When both groups are present, both
497
+ must match. Names are never used as identity filters.
498
+
499
+ The server worker is the action authority. By default it reads one bounded
500
+ page, accepts no more than five invitations per run and ten per UTC day,
501
+ jitters before every fresh preflight, serializes writes per sender, consumes
502
+ the shared LinkedIn daily budget, and stops on a warning, 403, 429, unknown
503
+ outcome, sender lock, or three consecutive errors. Successful acceptance is
504
+ written to the action audit and exact LinkedIn contact state as inbound,
505
+ invited, and connected, so later campaigns see the relationship.
506
+ A healthy worker run resets the consecutive provider-error streak; malformed
507
+ invitations remain visible in status even after later healthy runs.
508
+
509
+ Unipile currently places acceptance proof at `specifics.shared_secret` and
510
+ inviter data at
511
+ `inviter.{inviter_name,inviter_public_identifier,inviter_description}`.
512
+ SignalDash also reads the legacy top-level secret and old inviter fields. It
513
+ never returns or logs the secret. An invitation missing its ID, acceptance
514
+ proof, or an exact inviter identifier is not accepted: the server emits a
515
+ high-visibility error, stores only sanitized missing-field evidence, and
516
+ exposes it through `sd_auto_accept_status`. Three consecutive parse or provider
517
+ errors disable automation with `disabled_reason:"repeated_errors"`.
518
+
519
+ Use `sd_auto_accept_status` after changing the setting and whenever the user
520
+ asks whether it is healthy. Report `enabled`, exact filters, accepted today and
521
+ this week, failed attempts today, remaining dedicated and shared capacity,
522
+ `consecutive_errors`, `disabled_reason`, and every recent unparseable record.
523
+ Never describe a disabled or erroring worker as active.
524
+
525
+ ### Campaign time boundary
526
+
527
+ SignalDash does not expose a general message scheduler. The first executable
528
+ campaign scope has no user-selected future start date, recurring schedule,
529
+ automatic follow-up, acceptance-triggered message, or multi-message
530
+ ("double text") sequence. Do not claim that any such action was queued.
531
+
532
+ The common server write authority enforces the design-approved time controls
533
+ for every campaign action:
534
+
535
+ - a valid sender IANA timezone is mandatory;
536
+ - actions run only Monday through Friday, 09:00-17:00 sender-local time;
537
+ - every attempted action atomically stores the next per-sender pacing time,
538
+ randomized from 90 to 180 seconds;
539
+ - timing is checked before final provider preflight and again in the same
540
+ SQLite transaction that reserves budget and acquires the sender lease;
541
+ - one provider write may be in flight per sender;
542
+ - downtime, a work-window boundary, or a UTC reset never creates a catch-up
543
+ burst.
544
+
545
+ Manual one-object tools keep their existing exact-read, approval, duplicate,
546
+ rate, ownership, contact-state, and sender-lock guards. A later message after
547
+ an invitation or reply is new context and requires a fresh thread read, exact
548
+ draft, and human approval.
549
+
550
+ ### Human-approved invitation batches
551
+
552
+ An invitation batch is the only executable campaign form. It contains 1–10
553
+ exact canonical LinkedIn Classic profile URLs, one exact inclusion reason per
554
+ target, and one optional exact invitation note per target. The note limit is
555
+ 200 Unicode code points. Source labels, reasons, and notes reject leading or
556
+ trailing whitespace; notes normalize line endings and reject control or format
557
+ characters.
558
+
559
+ The human can describe the desired batch naturally, but the agent must
560
+ structure only facts the human explicitly supplied. Never infer or generate a
561
+ profile URL, inclusion reason, or note. Search, discovery, reactions, comments,
562
+ and connection exports are not batch target sources. The server performs no
563
+ runtime target or text generation.
564
+
565
+ Use the batch tools in this order:
566
+
567
+ 1. Call `li_create_invitation_batch` once with the exact source label, sender
568
+ IANA timezone, and complete ordered target list.
569
+ 2. Poll only with `li_get_invitation_batch` until the durable preview is
570
+ `previewed` or terminal. Inspect every target, including exclusions.
571
+ 3. Relay the returned `approval_url` to the human. The human opens it in a
572
+ browser, reauthenticates with the SignalDash invite credential, checks each
573
+ desired target (none are preselected), acknowledges the consequences, and
574
+ approves. MCP and agent bearer tokens cannot approve. Five failed credential
575
+ attempts durably block that batch's authentication surface for 15 minutes.
576
+ 4. Continue read-only status checks with `li_get_invitation_batch`. The worker
577
+ submits selected targets in fixed order only while the 24-hour approval,
578
+ sender binding, weekday work window, pacing, budgets, exact identity,
579
+ contact state, invitation state, chat absence, and tenant breaker remain
580
+ valid.
581
+ 5. To stop, first inspect the batch, show the exact state and hash, obtain
582
+ cancellation approval, then call `li_cancel_invitation_batch` once with
583
+ that hash and `confirm:true`.
584
+
585
+ Cancel permanently terminates the batch and affects only unstarted targets.
586
+ It cannot recall an executing provider action. A restart requires a new batch,
587
+ preview, and human approval. There is no pause/resume, priority, future start,
588
+ recurring schedule, automatic follow-up, acceptance polling, acceptance
589
+ message, multi-message sequence, or action after a successful invitation.
590
+
591
+ Unknown outcomes, provider warnings, checkpoints, HTTP 403, and HTTP 429 stop
592
+ the batch and lock the sender. Two campaign `outcome_unknown` writes on
593
+ different senders within five minutes open the persisted tenant breaker.
594
+ Tenant provider authentication failures open it immediately. While open, the
595
+ breaker rejects new batch creation and stops queued preview work before any
596
+ provider read.
597
+ Definite provider rejections consume attempts but can leave later exact targets
598
+ eligible to run. SignalDash limits are not LinkedIn-safe thresholds, and
599
+ native LinkedIn activity can still race the final reads.
600
+
601
+ ### Search your stored LinkedIn network
602
+
603
+ `li_search_connections` is the primary list-building path before any external
604
+ discovery. It is account-scoped and searches the stored connection sidecar
605
+ only. The request path performs zero LinkedIn, Unipile, HarvestAPI, or other
606
+ paid API calls and never starts or resumes a sync.
607
+
608
+ The required `query` matches accent-insensitive terms across name, headline,
609
+ and stored company. Optional `company` and `headline_keyword` filters are
610
+ case-insensitive substrings. `connected_after` and `connected_before` use
611
+ inclusive `YYYY-MM-DD` bounds. Legacy v1 relation rows do not contain a
612
+ separate company field, so their company filter is transparently matched
613
+ against the returned headline.
614
+
615
+ Each result contains only:
616
+
617
+ - `name`, `headline`, `public_id`, `profile_url`, and `connected_at`;
618
+ - `already_in_contact`, based on exact recorded inbound, outbound, reply, or
619
+ invitation history;
620
+ - the joined `contact_state`, `campaign_eligible`, and `blocking_reasons`.
621
+
622
+ The join checks only the row's exact LinkedIn public identifier and canonical
623
+ profile URL for this SignalDash user. It does not infer a person, merge aliases,
624
+ or inspect another tenant. Search results are planning evidence, not permission
625
+ to message or invite anyone. A later action still requires its exact
626
+ one-object read/preview, human review, approval, fresh preflight, and server
627
+ guards. The executable invitation-batch design does not accept a search result
628
+ as a target input.
629
+
630
+ When no connection rows are stored, the tool returns
631
+ `connections_not_synced` or `connections_sync_pending` and points to:
632
+
633
+ ```bash
634
+ npx -y @floomhq/signaldash connections linkedin-connections.csv
635
+ ```
636
+
637
+ Use that paced, resumable sync. Never replace the missing snapshot with a burst
638
+ of profile reads.
639
+
640
+ ### Capped discovery beyond the stored network
641
+
642
+ `li_discover_people` is the only supported paid discovery path. It searches
643
+ public LinkedIn profile cards through HarvestAPI without using the connected
644
+ LinkedIn sender. It does not fetch full profiles, find email addresses, send
645
+ anything, or create invitation-batch targets.
646
+
647
+ The server enforces local-first behavior. A completed paced connection snapshot
648
+ is mandatory. SignalDash searches that snapshot with the requested role/title
649
+ and company before any paid request. When local matches exist, it returns those
650
+ matches with `source:"own_network"` and `provider_requests:0`; use
651
+ `li_search_connections` to refine them. An incomplete snapshot returns
652
+ `connections_sync_required` and starts no provider request.
653
+
654
+ When there are no local matches:
655
+
656
+ 1. Call `li_discover_people` without `confirm`. The response previews the exact
657
+ query, filters, result limit, ten-profile maximum billing exposure, maximum
658
+ reserved cost, and 30-minute expiry. It makes zero paid requests.
659
+ 2. Show the human that exact paid request and maximum reserved cost. Obtain
660
+ explicit approval.
661
+ 3. Repeat the identical arguments once with `confirm:true`.
662
+
663
+ The hosted server then makes at most one serialized
664
+ `GET /linkedin/profile-search` request for page 1. It reserves the configured
665
+ worst-case page cost in integer micro-dollars before the request and enforces a
666
+ durable per-user cooldown, per-user daily request cap, tenant-wide daily
667
+ request cap, and tenant-wide daily cost cap. The reservation consumes the exact
668
+ preview atomically. Another paid request, including one after a failed or
669
+ ambiguous provider result, requires a fresh preview and approval. Failed or
670
+ ambiguous provider requests retain their reservation and are never retried
671
+ automatically.
672
+
673
+ SignalDash removes hidden `"LinkedIn Member"` cards, malformed or ambiguous
674
+ identities, duplicates, exact stored connections, and exact contact-state rows
675
+ already marked connected. Returned cards contain only name, headline,
676
+ location, public identifier, canonical profile URL, and exact local contact
677
+ state. No fuzzy or cross-channel identity merge occurs.
678
+
679
+ Discovery results are planning evidence only. `CAMPAIGN-DESIGN.md` excludes
680
+ searches and discovery as invitation-batch target sources. A later action on
681
+ one exact person begins the complete one-object preview, approval, fresh
682
+ preflight, contact-state, duplicate, ownership, and rate-limit flow.
683
+
357
684
  ### LinkedIn connections export via CLI
358
685
 
359
- This is the fourteenth operation. It is intentionally a paced CLI workflow, not
360
- an MCP bulk-read tool.
686
+ This is intentionally a paced CLI workflow, not an MCP bulk-read tool.
361
687
 
362
688
  ```bash
363
689
  npx -y @floomhq/signaldash connections linkedin-connections.csv
@@ -389,8 +715,9 @@ different calling order cannot bypass them.
389
715
  ### 428 `read_before_send_required`
390
716
 
391
717
  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.
718
+ email recipient was not present in a recently read thread, or an invitation
719
+ write lacks its exact recent preview/list read. The default read window is 30
720
+ minutes.
394
721
 
395
722
  Comply:
396
723
 
@@ -417,6 +744,41 @@ Comply:
417
744
 
418
745
  Changing whitespace or punctuation to evade the duplicate guard is prohibited.
419
746
 
747
+ ### 409 `contact_suppressed`
748
+
749
+ Meaning: one of the exact channel identifiers for this contact has an active
750
+ suppression. Message and invitation writes stop before the provider write.
751
+
752
+ Comply:
753
+
754
+ 1. Stop the action.
755
+ 2. Inspect the exact state with `sd_contact_state`.
756
+ 3. Do not switch identifiers, channels, accounts, or sessions to evade it.
757
+ 4. Escalate a mistaken suppression to the SignalDash operator. MCP cannot clear
758
+ it.
759
+
760
+ Campaign-mode actions also reject `recipient_replied` and `already_contacted`.
761
+ These blocks prevent automated follow-up after a known inbound message and
762
+ prevent another campaign from touching the same exact identifier. They do not
763
+ claim that SignalDash has resolved a person across channels.
764
+
765
+ ### 409 invitation and context preflight blocks
766
+
767
+ `thread_changed`, `already_connected`, `existing_conversation`,
768
+ `conversation_state_incomplete`, `invitation_already_pending`,
769
+ `inbound_invitation_pending`, `invitation_not_pending`,
770
+ `invitation_state_incomplete`, and `relationship_unverified` mean the exact
771
+ provider state no longer authorizes the action. New-invitation preflight checks
772
+ up to 250 sent invitations, up to 100 received invitations, and the exact
773
+ target's attendee-scoped chats. Any pagination cursor makes absence unproved.
774
+
775
+ Comply:
776
+
777
+ 1. Stop the action.
778
+ 2. Re-read the exact thread or invitation list.
779
+ 3. Do not expand pagination or fetch profiles in a loop.
780
+ 4. Obtain new approval only for a newly previewed exact action.
781
+
420
782
  ### 429 `rate_limit_exceeded`
421
783
 
422
784
  Meaning: the persisted daily action cap is exhausted. The response includes
@@ -431,21 +793,54 @@ Comply:
431
793
  to work around the cap.
432
794
  4. Do not queue a burst for the reset boundary.
433
795
 
796
+ Invitation sends also return `invitation_rate_limit_exceeded` when the separate
797
+ weekly policy is exhausted. The default policy is 100 attempts per sender,
798
+ resetting Monday at 00:00 UTC. It is a SignalDash policy, not a claim about a
799
+ LinkedIn-safe threshold.
800
+
801
+ Paid discovery returns `paid_discovery_cooldown`,
802
+ `paid_discovery_user_daily_cap`, `paid_discovery_tenant_daily_cap`, or
803
+ `paid_discovery_cost_cap` before the HarvestAPI request when its pacing,
804
+ request, or spend boundary is reached. Stop and use `retry_at` or the UTC reset
805
+ reported by the server. Do not switch users, sessions, or machines to bypass a
806
+ paid-provider cap.
807
+
808
+ ### 423 sender locks and 502 `outcome_unknown`
809
+
810
+ A provider warning, HTTP 403, HTTP 429, sender identity mismatch, or ambiguous
811
+ provider outcome locks all later LinkedIn writes for that logical sender. An
812
+ ambiguous result returns `outcome_unknown`, consumes the action budget, and is
813
+ never retryable.
814
+
815
+ Comply:
816
+
817
+ 1. Do not retry or switch sessions.
818
+ 2. Inspect the provider account manually for a restriction and verify whether
819
+ the action landed.
820
+ 3. Escalate for human reconciliation. Only the hosted operator can clear the
821
+ durable lock after verification.
822
+
434
823
  Any upstream 429, provider warning, checkpoint, restriction, unusual-activity
435
824
  prompt, or HTTP 403 also means stop. Do not retry.
436
825
 
437
826
  ## Before every send
438
827
 
439
- These rules apply to LinkedIn, WhatsApp, email, and public LinkedIn posts.
828
+ These rules apply to LinkedIn messages and invitations, WhatsApp, email, and
829
+ public LinkedIn posts.
440
830
 
441
831
  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.
832
+ recipient or audience and the exact final text. The only standing-action
833
+ exception is invitation auto-accept after explicit approval of its exact
834
+ persistent setting and filters.
835
+ 2. Read the exact thread immediately before a message action. For invitations,
836
+ preview the exact target and note or list the exact current invitation.
837
+ Check the recipient, latest inbound message, prior context, relationship,
838
+ pending state, and whether the proposed action already exists.
446
839
  3. Never infer a recipient from a partial name. Resolve duplicate names with
447
840
  the user.
448
- 4. Never bulk-send, fan out, loop over people, or parallelize actions.
841
+ 4. Never loop or parallelize single-object send tools. Only the immutable
842
+ invitation-batch runner may submit more than one target, and only after the
843
+ separate exact browser approval.
449
844
  5. Never turn reactions, comments, connections, or exported rows into an
450
845
  unsolicited outreach list.
451
846
  6. Send one message at human pace. Let server pacing finish.
@@ -456,9 +851,11 @@ These rules apply to LinkedIn, WhatsApp, email, and public LinkedIn posts.
456
851
  9. Confirm delivery by reading the thread after the action.
457
852
  10. Account health outranks throughput and task completion.
458
853
 
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
854
+ The hosted backend defaults to 20 LinkedIn action attempts per stable logical
855
+ sender per UTC day, shared across messages, invitations, invitation
856
+ maintenance, and post publishing. Invitation sends also use the default
857
+ 100-attempt weekly policy. Email and WhatsApp retain their hosted send caps. A
858
+ deployment can configure different caps. Never promise a particular remaining
462
859
  allowance until the response reports `rate_limit.limit` and `remaining`.
463
860
 
464
861
  ## Worked flows
@@ -529,7 +926,9 @@ User: "Who engaged with my last LinkedIn post, and what should I do?"
529
926
  Do not invent profile facts absent from the result.
530
927
  6. If the user asks to message one person, locate the exact existing chat,
531
928
  read it, draft a contextual message, obtain exact approval, re-read, and
532
- send once. SignalDash cannot invite or mass-message reactors.
929
+ send once. If the user explicitly asks to invite one exact reactor, resolve
930
+ the exact provider ID, run the invitation preview, obtain exact approval,
931
+ and send once. Engagement never authorizes an invitation or a loop.
533
932
 
534
933
  ### Flow 3: find and reply to a WhatsApp thread
535
934
 
@@ -572,7 +971,106 @@ User: "Find my WhatsApp thread with Sara and reply that 16:30 works."
572
971
  8. Read the chat again and confirm the text appears once as an outbound
573
972
  message.
574
973
 
575
- ### Flow 4: export LinkedIn connections
974
+ ### Flow 4: send, accept, or withdraw one LinkedIn invitation
975
+
976
+ For a new invitation:
977
+
978
+ 1. Resolve one exact `provider_id`. A display name or fuzzy match is
979
+ insufficient.
980
+ 2. Preview without confirmation:
981
+
982
+ ```text
983
+ li_send_invitation({
984
+ "provider_id":"ACoAAExactMember",
985
+ "note":"Hi Amina, I enjoyed your post on agent safety."
986
+ })
987
+ ```
988
+
989
+ 3. Show the server-confirmed sender, target, exact note, and character count.
990
+ 4. Obtain approval for that exact target and note.
991
+ 5. Repeat the identical payload once with `"confirm":true`.
992
+ 6. Stop on relationship, existing-conversation, incomplete-state,
993
+ pending-invitation, cap, warning, lock, or `outcome_unknown` errors. Never
994
+ retry an ambiguous result.
995
+
996
+ For received or sent invitation maintenance:
997
+
998
+ 1. Call `li_invitations_received` or `li_invitations_sent`.
999
+ 2. Resolve the exact invitation ID and show the exact person and action.
1000
+ 3. Obtain approval.
1001
+ 4. Call `li_accept_invitation` or `li_withdraw_invitation` once with that ID
1002
+ and `"confirm":true`.
1003
+ 5. List again to verify that the invitation is no longer pending.
1004
+
1005
+ Never loop over the returned page. Incoming acceptance and stale withdrawal
1006
+ are human-approved, single-object operations only.
1007
+
1008
+ ### Flow 4b: enable and inspect invitation auto-accept
1009
+
1010
+ User: "Auto-accept LinkedIn invitations from these exact public IDs when their
1011
+ description contains Founder."
1012
+
1013
+ 1. Call `sd_settings_get({})`, then show the exact persistent setting:
1014
+
1015
+ ```text
1016
+ Enabled: true
1017
+ Public identifiers: amina-rahman, marco-silva
1018
+ Description keywords: Founder
1019
+ Matching rule: exact public ID AND description keyword
1020
+ ```
1021
+
1022
+ 2. Obtain explicit approval, then call once:
1023
+
1024
+ ```text
1025
+ sd_settings_set({
1026
+ "auto_accept_linkedin":true,
1027
+ "auto_accept_linkedin_filters":{
1028
+ "public_identifiers":["amina-rahman","marco-silva"],
1029
+ "description_keywords":["Founder"]
1030
+ },
1031
+ "confirm":true
1032
+ })
1033
+ ```
1034
+
1035
+ 3. Verify with:
1036
+
1037
+ ```text
1038
+ sd_auto_accept_status({})
1039
+ ```
1040
+
1041
+ 4. Report the setting, accepted count today and this week, failed attempts
1042
+ today, both remaining capacities, repeated-error state, and every
1043
+ unparseable invitation. If the worker disabled itself, stop and surface the
1044
+ exact `disabled_reason` and missing fields. Do not silently re-enable it.
1045
+ 5. To disable, obtain explicit approval and call
1046
+ `sd_settings_set({"auto_accept_linkedin":false,"confirm":true})`, then
1047
+ verify both `sd_settings_get({})` and status.
1048
+
1049
+ ### Flow 5: search LinkedIn connections locally
1050
+
1051
+ User: "Find product leaders at Acme in my LinkedIn network."
1052
+
1053
+ 1. Call:
1054
+
1055
+ ```text
1056
+ li_search_connections({
1057
+ "query":"product leader",
1058
+ "filters":{"company":"Acme"},
1059
+ "limit":20
1060
+ })
1061
+ ```
1062
+
1063
+ 2. Confirm the response has `local_only:true` and `provider_requests:0`.
1064
+ 3. Return only the stored profile fields and exact contact-state result. Do not
1065
+ add inferred profile facts or cross-channel identity.
1066
+ 4. If the response says `connections_not_synced` or
1067
+ `connections_sync_pending`, relay its paced-sync instruction. Do not trigger
1068
+ profile reads or discovery as a substitute.
1069
+ 5. Stop after the read-only results. A later action on one exact person starts
1070
+ the complete guarded one-object flow with fresh provider context and
1071
+ approval. Never turn the result page into a send loop.
1072
+
1073
+ ### Flow 6: export LinkedIn connections
576
1074
 
577
1075
  User: "Export my LinkedIn connections to CSV."
578
1076
 
@@ -595,11 +1093,88 @@ User: "Export my LinkedIn connections to CSV."
595
1093
  6. Report the saved path and row count. If the CLI reports a paused or partial
596
1094
  sync, say that the export is partial and rerun later to resume.
597
1095
 
1096
+ ### Flow 7: discover people beyond the stored network
1097
+
1098
+ User: "Find platform engineers at Acme in Berlin beyond my network."
1099
+
1100
+ 1. Call without confirmation:
1101
+
1102
+ ```text
1103
+ li_discover_people({
1104
+ "query":"Platform Engineer",
1105
+ "filters":{"company":"Acme","location":"Berlin"},
1106
+ "limit":5
1107
+ })
1108
+ ```
1109
+
1110
+ 2. If it returns `source:"own_network"`, return those local matches and stop.
1111
+ The paid provider was not called.
1112
+ 3. If it returns `connections_sync_required`, run or resume the paced
1113
+ connections sync and stop. Do not bypass it.
1114
+ 4. If it returns a paid preview, show the exact query, filters, result limit,
1115
+ and `maximum_cost_reserved_usd`. Obtain explicit approval.
1116
+ 5. Repeat the identical payload once with `"confirm":true`.
1117
+ 6. Report only the returned public profile-card fields and exact contact state.
1118
+ State that hidden/unusable and exact already-connected cards were excluded.
1119
+ 7. Stop after the read-only results. Do not enrich profiles, find emails,
1120
+ create a batch, or loop into invitations or messages.
1121
+
1122
+ ### Flow 8: create, review, or cancel one invitation batch
1123
+
1124
+ User: "Invite these two exact LinkedIn profiles with these notes."
1125
+
1126
+ 1. Confirm the human supplied every canonical profile URL, exact inclusion
1127
+ reason, exact optional note, source label, and sender timezone. Ask for any
1128
+ missing value. Do not fill gaps with search or generation.
1129
+ 2. Call:
1130
+
1131
+ ```text
1132
+ li_create_invitation_batch({
1133
+ "source_label":"User-approved event follow-up",
1134
+ "time_zone":"Europe/Berlin",
1135
+ "targets":[
1136
+ {
1137
+ "profile_url":"https://www.linkedin.com/in/amina-rahman/",
1138
+ "inclusion_reason":"The user met Amina at the named event",
1139
+ "note":"Hi Amina, great meeting you at the agent safety meetup."
1140
+ }
1141
+ ]
1142
+ })
1143
+ ```
1144
+
1145
+ 3. Store the exact `batch_id`. Call `li_get_invitation_batch` until its state
1146
+ is `previewed` or terminal. Report every excluded row and reason.
1147
+ 4. Show the sender, source, ordered targets, full profile URLs, reasons, exact
1148
+ notes, reused-copy counts, current limits, timing, expiry, cancellation
1149
+ limit, and residual native-activity race.
1150
+ 5. Relay the exact `approval_url`. Do not fetch, submit, or automate that
1151
+ browser page. The human reauthenticates and selects the desired unchecked
1152
+ rows.
1153
+ 6. Inspect the batch to report `approved`, `running`, or terminal progress.
1154
+ Never interpret an approval-page visit as approval; only server state proves
1155
+ it.
1156
+ 7. For cancellation, inspect immediately, show the exact state and
1157
+ `approval_view_hash`, obtain explicit cancellation approval, then call:
1158
+
1159
+ ```text
1160
+ li_cancel_invitation_batch({
1161
+ "batch_id":"00000000-0000-4000-8000-000000000000",
1162
+ "approval_view_hash":"the exact hash from the fresh read",
1163
+ "confirm":true
1164
+ })
1165
+ ```
1166
+
1167
+ 8. Report that planned targets were cancelled and any executing action was not
1168
+ recalled. Never claim the batch was paused or can resume.
1169
+
598
1170
  ## Read-only requests and drafts
599
1171
 
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.
1172
+ Read-only account work does not require send approval. You can list, read,
1173
+ search the stored connection snapshot, summarize, analyze engagement, and
1174
+ create a non-published post draft as requested. Paid discovery separately
1175
+ requires approval of its exact preview and maximum reserved cost. Still use
1176
+ narrow limits, protect private content, and avoid bulk collection. Search and
1177
+ discovery results do not authorize an outbound action.
603
1178
 
604
1179
  For every draft, label it clearly as a draft. Never present a suggested reply
605
1180
  as sent. Never present `{"drafted":true,"published":false}` as a published post.
@@ -628,11 +1203,60 @@ Before reporting completion, verify:
628
1203
  - The skill is persisted in a discovered skill directory.
629
1204
  - `status` proves the required provider is connected.
630
1205
  - The exact chat, thread, post, or export path was resolved.
1206
+ - A connection search reported `local_only:true`, made zero provider requests,
1207
+ and did not authorize an outbound action.
1208
+ - A paid discovery had a completed connection snapshot, returned no own-network
1209
+ matches, used an unexpired exact preview and approval, made at most one
1210
+ serialized provider request, reported its cost budget, and remained planning
1211
+ evidence only.
631
1212
  - Every send used a fresh exact-thread read and exact human approval.
1213
+ - Every invitation send used an exact server preview and exact approval.
1214
+ - Every invitation accept or withdrawal used a fresh exact invitation-list
1215
+ read and exact approval.
1216
+ - Invitation auto-accept was explicitly enabled for the exact user, preserved
1217
+ both the dedicated and shared caps, stopped on repeated errors, and exposed
1218
+ every unparseable invitation through `sd_auto_accept_status`.
1219
+ - Every invitation batch used only exact user-supplied canonical profile URLs,
1220
+ reasons, and notes; exposed every target; used the separate browser approval;
1221
+ and never approved through MCP.
1222
+ - Every batch cancel followed a fresh exact batch read and stated that an
1223
+ executing action cannot be recalled.
632
1224
  - No duplicate, bulk, parallel, warning, 403, or 429 path was bypassed.
1225
+ - No sender lock or `outcome_unknown` result was retried.
633
1226
  - A send was confirmed by a post-send read.
634
1227
  - A post draft remained unpublished unless `publish:true` was explicitly
635
1228
  approved.
636
1229
  - An export ended with `+ saved` and a non-empty CSV.
637
1230
 
638
1231
  If any item is unverified, state exactly what remains incomplete.
1232
+
1233
+ ## How to write the message (this is where agents fail hardest)
1234
+
1235
+ Agents pad. Padding is the clearest tell that a human did not write it, and on
1236
+ LinkedIn or WhatsApp it gets ignored. Less is more, always.
1237
+
1238
+ - **Short.** A reply is usually 1-3 sentences. If they wrote one line, reply
1239
+ with one line. Match the length and register of the thread.
1240
+ - **One idea per message.** Do not stack context, ask and pleasantry into one
1241
+ block. Split into 2-3 short consecutive sends instead of one paragraph.
1242
+ - **No preamble, no summary-back.** Never "I hope this finds you well", never
1243
+ restate what they just said, never a formal sign-off in a chat.
1244
+ - **No em dashes.** Use commas, periods, colons.
1245
+ - **No hype filler.** Cut "excited to", "reaching out", "just wanted to",
1246
+ "circling back", "leverage", "synergies", and any eager closer.
1247
+ - **Their language.** German thread stays German, with real umlauts (für, not
1248
+ fuer). Never translate their language away.
1249
+ - **Read the thread first, then sound like the user.** Their own recent messages
1250
+ in that thread are the style reference. Copy that register, not a template.
1251
+
1252
+ Before/after, same intent:
1253
+
1254
+ > Bad: "Hi Mircea, I hope you're doing well! Thanks so much for reaching out
1255
+ > about scheduling a call. I'd be delighted to connect and would love to explore
1256
+ > how we might be able to work together. Please let me know what times work best
1257
+ > for you and I'll do my best to accommodate your schedule."
1258
+
1259
+ > Good: "hey Mircea, ja gerne. hier mein link: [cal]"
1260
+
1261
+ If a draft is longer than the thread's own messages, cut it before showing it.
1262
+