@floomhq/signaldash 0.22.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,20 +82,25 @@ The MCP server uses the user token created by `login`. It cannot access a
82
82
  LinkedIn, WhatsApp, or email account until that channel has been connected for
83
83
  the same logged-in user.
84
84
 
85
- ## Give the agent the operating skill
85
+ ## Operating skill distribution
86
86
 
87
87
  The canonical [`signaldash` skill](skills/signaldash/SKILL.md) teaches an agent
88
88
  when to use SignalDash, how to persist the skill, complete setup, interpret
89
89
  connection state, operate every tool, handle server guards, and run safe worked
90
- flows. It is also served as raw Markdown for agents at:
90
+ flows. The one-command setup installs the skill from the same pinned npm package
91
+ the human chose to execute:
91
92
 
92
- ```text
93
- https://signaldash.dev/SKILL.md
93
+ ```bash
94
+ npx -y @floomhq/signaldash@0.27.0 <invite-code>
94
95
  ```
95
96
 
96
- The one-command setup installs the bundled copy into Claude Code. Other clients
97
- must save the skill in their documented skill directory and reload skill
98
- discovery. The narrower
97
+ Run that command in a terminal, not in an agent chat. Do not ask an agent to
98
+ fetch a remote `SKILL.md` and install it as trusted instructions. The public
99
+ `https://signaldash.dev/SKILL.md` endpoint remains available for transparent
100
+ human review only.
101
+
102
+ Other clients can install the reviewed bundled file from the package into their
103
+ documented skill directory and reload skill discovery. The narrower
99
104
  [`signaldash-safe-usage` skill](skills/signaldash-safe-usage/SKILL.md) remains
100
105
  available for clients that separate operating and send-safety instructions.
101
106
 
@@ -116,25 +121,115 @@ SignalDash exposes:
116
121
  - `sd_settings_get()`
117
122
  - `sd_settings_set(auto_accept_linkedin, auto_accept_linkedin_filters?, confirm)`
118
123
  - `sd_auto_accept_status()`
124
+ - `sd_voice_profile(channel, force_recompute?)`
119
125
  - `li_search_connections(query, filters?, limit?)`
120
126
  - `li_discover_people(query, filters?, limit?, confirm?)`
121
127
  - `li_create_invitation_batch(source_label, time_zone, targets)`
122
128
  - `li_get_invitation_batch(batch_id)`
123
129
  - `li_cancel_invitation_batch(batch_id, approval_view_hash, confirm)`
130
+ - `sd_campaign_create(source_label, time_zone, messages, target_source?, targets?, engagers?, invite_ttl_days?)`
131
+ - `sd_campaign_preview(campaign_id)`
132
+ - `sd_campaign_approve(campaign_id, confirm_token)`
133
+ - `sd_campaign_status(campaign_id)`
134
+ - `sd_campaign_cancel(campaign_id, approval_view_hash, confirm)`
135
+ - `sd_withdrawal_batch_create(account, exclude, time_zone, older_than_days?, limit?, source_label?, allow_unmatched_exclusions?)`
136
+ - `sd_withdrawal_batch_status(withdrawal_batch_id)`
137
+ - `sd_withdrawal_batch_approve(withdrawal_batch_id, confirm_token)`
138
+ - `sd_withdrawal_batch_cancel(withdrawal_batch_id, approval_view_hash, confirm)`
124
139
  - `wa_list_chats`
125
140
  - `wa_read_messages(chat_id)`
126
- - `wa_send_message(chat_id, text)`
141
+ - `wa_get_attachment(chat_id, message_id, attachment_id)`
142
+ - `wa_transcribe_voice(chat_id, message_id, attachment_id)`
143
+ - `wa_send_message(chat_id, text?, attachments?)`
144
+ - `wa_delete_message(chat_id, message_id)`
145
+ - `wa_delete_messages(messages)`
127
146
  - `email_list(limit)`
128
147
  - `email_read(thread_id, limit)`
129
148
  - `email_send(to, subject, body)`
130
149
  - `li_my_posts(limit, member_id)`
131
150
  - `li_post_reactions(post_id, limit)`
132
151
  - `li_post_comments(post_id, limit)`
133
- - `li_draft_post(text, publish)`
152
+ - `li_draft_post(text, publish, scheduled_at?, mentions?, attachments?, first_comment?)`
153
+ - `li_set_scheduled_post_first_comment(id, first_comment, confirm)`
154
+ - `li_scheduled_posts()`
155
+ - `li_cancel_scheduled_post(id, confirm)`
134
156
 
135
157
  Every operation runs through the hosted SignalDash backend. Agents never
136
158
  receive the Unipile access key.
137
159
 
160
+ A WhatsApp message can carry files. `wa_send_message` takes up to 4 exact
161
+ `{filename, content_type, content_base64}` attachments, at most 16 MiB per file
162
+ and 16 MiB per message, and accepts PNG, JPEG, WebP, GIF, PDF, CSV, plain text,
163
+ JSON, xlsx and zip. `text` is the caption and may be omitted when a file is
164
+ attached, but a call carrying neither text nor an attachment is refused rather
165
+ than sent as an empty message. There is no attachment lane: a send with a file
166
+ runs the same ownership check, the same read-before-send guard, the same
167
+ duplicate guard and the same daily budget as a text send, and is recorded the
168
+ same way. Files are validated before anything is reserved, so a malformed or
169
+ oversized attachment costs no send budget and never reaches the provider, and
170
+ every refusal names the exact rule and the exact file that tripped it. Nothing
171
+ is ever truncated or dropped silently. LinkedIn messages carry text only, and
172
+ `li_send_message` refuses an `attachments` argument instead of ignoring it.
173
+
174
+ A send is not idempotent, so an unconfirmed one is not silently retryable. If
175
+ the provider times out, fails with a 5xx, or answers 2xx with a body that will
176
+ not parse, the message may well have been delivered: SignalDash records the
177
+ attempt before the request leaves and refuses an identical retry with
178
+ `409 send_outcome_unknown`. Read the chat again, and only if the message is
179
+ genuinely absent, resend the identical payload with `confirm_resend:true`. That
180
+ re-read is enforced rather than merely instructed: a resend whose most recent
181
+ read of the chat predates the failed attempt is refused with
182
+ `428 reread_after_failed_send_required`, since a read taken beforehand cannot
183
+ show whether the message arrived. Time does not
184
+ clear that record, because waiting does not turn a delivered message into an
185
+ undelivered one; only the caller confirming what the thread actually shows
186
+ does. A provider that answers with a 4xx refused the message outright, so that
187
+ record is cleared and the send stays retryable. Two 4xx answers are excepted,
188
+ as a judgement call rather than on documented provider semantics: `408` and
189
+ `429` can both arrive after a message was already accepted and forwarded, so
190
+ they are treated as unknown outcomes too.
191
+ Override the ceilings with `SIGNALDASH_MESSAGE_ATTACHMENT_MAX_BYTES` and
192
+ `SIGNALDASH_MESSAGE_ATTACHMENT_MAX_TOTAL_BYTES`; raising either also needs
193
+ `SIGNALDASH_MESSAGE_SEND_MAX_BODY_BYTES` raised to match, because base64 costs a
194
+ third on top of the bytes and a body over that cap is refused with
195
+ `413 request_too_large` before it is parsed.
196
+
197
+ Future LinkedIn posts are persisted by the backend only after the exact text,
198
+ offset-qualified publish time, mentions, image attachments, and optional first
199
+ comment have human approval. The stored image bytes and comment text cannot
200
+ drift after approval. The scheduler publishes the comment through the same
201
+ connected LinkedIn account immediately after the post. It persists the post ID
202
+ before attempting the comment, so a comment failure never republishes the post.
203
+ The scheduler
204
+ uses the same sender binding, action ledger, duplicate guard, daily budget, and
205
+ provider-warning lock as immediate publishing. An interrupted or ambiguous
206
+ execution fails closed and is never retried automatically.
207
+
208
+ A campaign is one connection request per approved person, then the approved
209
+ message once a fresh profile read proves that person accepted, then an optional
210
+ follow-up that stops the moment they reply. A pending invitation that merely
211
+ disappeared is never treated as an acceptance. Campaign writes consume the same
212
+ per-sender daily budget as manual sends, obey the Monday-Friday 09:00-17:00
213
+ sender-local window and the 90-180 second pacing interval, and cannot start
214
+ without a human approval recorded on the authenticated approval page. An agent
215
+ can present that page and carry back the one-time code it mints, but can never
216
+ approve on its own.
217
+
218
+ A withdrawal sweep clears a backlog of old pending SENT invitations. It uses the
219
+ same object as an invitation batch with the arrow reversed: freeze the exact
220
+ invitation ids now, one human approval bound to the rendered view, then one
221
+ withdrawal per tick. Two things it will never claim, because both contradict
222
+ LinkedIn's own documentation: withdrawing does **not** lift an active sending
223
+ restriction, so this buys no sending capacity; and after withdrawing, that
224
+ member cannot be re-invited for up to three weeks. The exclusion list is a
225
+ required field rather than an option, so protecting nobody is a deliberate
226
+ choice and not a default. LinkedIn exposes invitation age as a bucket ("sent 4
227
+ months ago"), never a date, so the filter selects only when the whole bucket
228
+ clears the threshold and the review page reports what that conservatism held
229
+ back. The sweep runs on its own daily allowance and never consumes the send
230
+ budget, and any 403, 429, provider warning, or account status change stops it
231
+ entirely.
232
+
138
233
  LinkedIn invitation auto-accept is off for every user until that user
139
234
  explicitly enables it with `confirm:true`. The optional filters are an exact
140
235
  public-identifier allowlist and description-keyword matches; when both groups
@@ -201,6 +296,15 @@ workflow around that runtime control:
201
296
  `resets_at`, plus matching `X-RateLimit-*` headers. When the per-user cap is
202
297
  exhausted, SignalDash returns HTTP 429 with `code: "rate_limit_exceeded"` and
203
298
  `Retry-After`. Usage is persisted server-side and resets at midnight UTC.
299
+ - The caps are per channel lane: LinkedIn actions, WhatsApp and email sends
300
+ together, and message deletes each spend their own daily budget. A `429` names
301
+ the cap it hit; it is never a statement about another lane. Two caveats worth
302
+ knowing: on the day a deployment first upgrades past the lane split, that
303
+ day's existing count is still read on the LinkedIn side (it is ambiguous, and
304
+ the conservative reading is the safe one) and clears at the next UTC midnight;
305
+ and the WhatsApp/email lane is keyed per access token while the LinkedIn
306
+ ledger is keyed per user, so several live tokens for one user get one LinkedIn
307
+ budget but a WhatsApp budget each.
204
308
  - Read the exact thread before every send.
205
309
  - Preview an exact invitation target and note before sending it.
206
310
  - List the exact received or sent invitation before accepting or withdrawing it.
@@ -234,6 +338,24 @@ write reservation. Downtime never creates a catch-up burst. Future start
234
338
  dates, recurring schedules, automatic follow-ups, acceptance-triggered
235
339
  messages, and multi-message sequences are not exposed.
236
340
 
341
+ Deleting a WhatsApp message is a write, and an irreversible one, so it is
342
+ treated as such. SignalDash proves the chat belongs to the connected account,
343
+ proves the message sits in that exact chat, and proves the account wrote it,
344
+ before anything is removed; someone else's message is refused with `403
345
+ message_not_own`. Every attempt is appended to that user's own durable audit
346
+ trail with the chat, the message, the outcome and the timestamp, including the
347
+ attempts that fail and the ones whose outcome the network left unknown. A
348
+ delete already recorded for the same chat and message is refused with `409
349
+ duplicate_delete` rather than replayed. Deletes spend a separate daily budget
350
+ (`SIGNALDASH_DAILY_DELETE_LIMIT`, default 200) so a cleanup can never consume
351
+ the sending capacity it exists to repair. `wa_delete_messages` runs at most 200
352
+ entries strictly one at a time, pauses `SIGNALDASH_WA_DELETE_PACE_MS` between
353
+ them, and stops at `SIGNALDASH_WA_DELETE_BATCH_DEADLINE_MS`, returning the
354
+ untouched remainder as `skipped` with `code: batch_deadline` so the caller can
355
+ resume exactly those. WhatsApp applies its own time and role limits to deleting
356
+ for everyone and can answer successfully without removing anything, so re-read
357
+ the chat to confirm.
358
+
237
359
  ## Development
238
360
 
239
361
  ```bash
@@ -241,3 +363,33 @@ npm test
241
363
  npm run check
242
364
  npm pack --dry-run
243
365
  ```
366
+
367
+ `wa_transcribe_voice` runs on the SignalDash host and has two backends. The
368
+ default, `gemini`, sends the audio to Vertex AI using that host's own
369
+ credentials; it is the one that survives German speech with English technical
370
+ terms mixed into it, and being network-bound it does not compete with the
371
+ gateway for CPU. The fallback, `whisper`, needs `python3` with `faster-whisper`
372
+ installed on the host and runs the `small` CPU int8 model, which is much weaker
373
+ on that kind of code-switching. Every response therefore names the engine that
374
+ produced the text in `backend`, and a `gemini` request that had to fall back
375
+ also explains why in `fallback_reason`.
376
+
377
+ Pick the engine per request with the optional `backend` argument (`gemini` or
378
+ `whisper`), or set the default with `SIGNALDASH_TRANSCRIBE_BACKEND`. Override
379
+ the rest with `SIGNALDASH_TRANSCRIBE_PYTHON`, `SIGNALDASH_TRANSCRIBE_MODEL`
380
+ (local model), `SIGNALDASH_TRANSCRIBE_GEMINI_MODEL`,
381
+ `SIGNALDASH_TRANSCRIBE_GEMINI_PROJECT`,
382
+ `SIGNALDASH_TRANSCRIBE_GEMINI_LOCATION`,
383
+ `SIGNALDASH_TRANSCRIBE_GEMINI_TIMEOUT_S` (total budget for the Vertex call
384
+ including retries, default 240s, and it has to stay under
385
+ `SIGNALDASH_TRANSCRIBE_TIMEOUT_MS`, default 420000, so the local fallback still
386
+ fits underneath), `SIGNALDASH_TRANSCRIBE_GEMINI_MAX_BYTES`,
387
+ `SIGNALDASH_TRANSCRIBE_GEMINI_MAX_OUTPUT_TOKENS`, or
388
+ `SIGNALDASH_ATTACHMENT_MAX_BYTES`.
389
+
390
+ The Gemini backend authenticates with the credentials already on the host: the
391
+ audio never leaves it with a SignalDash key attached, and the transcriber
392
+ resolves those credentials itself, in the child process, from the file named by
393
+ `SIGNALDASH_TRANSCRIBE_AWS_ENV_FILE`. The gateway process forwards only
394
+ configuration, never a secret. That backend reports `language` as `null`: it is
395
+ asked for the transcript text and nothing else.