@floomhq/signaldash 0.39.18 → 0.40.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
@@ -120,7 +120,7 @@ setup installs both from the same pinned npm package
120
120
  the human chose to execute:
121
121
 
122
122
  ```bash
123
- npx -y @floomhq/signaldash@0.39.18 <invite-code>
123
+ npx -y @floomhq/signaldash@0.40.0 <invite-code>
124
124
  ```
125
125
 
126
126
  Run that command in a terminal, not in an agent chat. Do not ask an agent to
@@ -145,24 +145,46 @@ SignalDash exposes:
145
145
  - `sd_inspiration_import(source, source_author?, federico_note?, purpose?, content_kind?, credit_required?, credit_note?, format_description?)`
146
146
  - `sd_inspiration_channel_configure(chat_id, provider_id, chat_name, confirm)`
147
147
  - `sd_inspiration_channel_set(capture_enabled, confirm)`
148
+ - `sd_x_signal_set(enabled, confirm)`
149
+ - `sd_x_signal_import(feed, claim_key, topic, title, claim_text, source, snapshot, confirm)`
150
+ - `sd_x_signal_add_evidence(id, expected_version, source_kind, source, confirm)`
151
+ - `sd_x_signal_propose(id, expected_version, proposed_text, proposed_claims, confirm)`
152
+ - `sd_x_signal_list(feed, state?, limit?)`
148
153
  - `sd_secretary_approve(disposition_id, payload_hash, confirm)`
149
154
  - `sd_secretary_reject(disposition_id, confirm)`
150
155
  - `li_list_chats`
151
156
  - `li_read_messages(chat_id)`
157
+ - `li_get_attachment(chat_id, message_id, attachment_id)`
152
158
  - `li_mark_read(chat_id)`
153
159
  - `li_start_chat(member_ids, text, confirm?, approval_hash?, dry_run?)`
154
- - `li_send_message(chat_id?, text?, expected_watermark?, mark_read?, secretary_receipt_id?)`
160
+ - `li_send_message(chat_id?, text?, expected_watermark?, mark_read?, secretary_receipt_id?)` — confirmed sends clear unread by default; `mark_read:false` preserves unread.
155
161
  - `li_send_invitation(provider_id, note?, confirm?)`
156
162
  - `li_invitations_received(limit?, cursor?)`
157
163
  - `li_accept_invitation(invitation_id, confirm)`
158
164
  - `li_invitations_sent(limit?, cursor?)`
159
165
  - `li_withdraw_invitation(invitation_id, confirm)`
166
+
167
+ The X signal collector is a manual, review-first command. It pins the exact
168
+ Apify actor build, caps each feed at 25 complete dataset rows and $0.05, stores
169
+ the exact raw row plus run/query provenance, and imports no LinkedIn draft:
170
+
171
+ ```bash
172
+ APIFY_API_TOKEN=... node scripts/collect-x-signals.mjs \
173
+ --feed fede --feed rocketlist --confirm
174
+ ```
175
+
176
+ The deployment capability and the authenticated SignalDash user's setting must
177
+ both be enabled. Re-running a completed observation is deterministic; a newer
178
+ observation adds an immutable engagement snapshot. The command creates no
179
+ timer, scheduler entry, Buffer item, or provider write.
160
180
  - `sd_contact_state(channel, identifiers, action?, reason?, confirm?)`
161
181
  - `sd_budget_status()`
162
182
  - `sd_settings_get()`
163
183
  - `sd_settings_set(auto_accept_linkedin, auto_accept_linkedin_filters?, confirm)`
164
184
  - `sd_auto_accept_status()`
165
185
  - `sd_voice_profile(channel, force_recompute?)`
186
+ - `sd_content_automation_report(automation_key, name, schedule, state, confirm, description?, detail?, step?, next_run_at?, artifact_kind?, artifact_id?)`
187
+ - `sd_content_automations()`
166
188
  - `li_search_connections(query, filters?, limit?)`
167
189
  - `li_discover_people(query, filters?, limit?, confirm?)`
168
190
  - `li_create_invitation_batch(source_label, time_zone, targets)`
@@ -182,8 +204,8 @@ SignalDash exposes:
182
204
  - `wa_mark_read(chat_id)`
183
205
  - `wa_get_attachment(chat_id, message_id, attachment_id)`
184
206
  - `wa_transcribe_voice(chat_id, message_id, attachment_id)`
185
- - `wa_start_chat(member_ids, text, confirm?, approval_hash?, dry_run?)`
186
- - `wa_send_message(chat_id, text?, attachments?, expected_watermark?, mark_read?)`
207
+ - `wa_start_chat(member_ids, text, subject?, confirm?, approval_hash?, dry_run?)`: `subject` names a new group (2+ members) and is bound into the approval.
208
+ - `wa_send_message(chat_id, text?, attachments?, expected_watermark?, mark_read?)` — confirmed sends clear unread by default; `mark_read:false` preserves unread.
187
209
  - `wa_delete_message(chat_id, message_id)`
188
210
  - `wa_delete_messages(messages)`
189
211
  - `email_list(limit)`
@@ -196,7 +218,7 @@ SignalDash exposes:
196
218
  - `li_like_comment(post_id, parent_comment_id, comment_id, expected_watermark?)`
197
219
  - `li_delete_message(chat_id, message_id, confirm)`
198
220
  - `li_delete_comment(post_id, comment_id, confirm)`
199
- - `li_draft_post(text, publish, scheduled_at?, content_pipeline_id?, content_pipeline_override?, mentions?, attachments?, first_comment?)`
221
+ - `li_draft_post(text, publish, scheduled_at?, content_pipeline_id?, content_pipeline_override?, mentions?, attachments?, first_comment?, confirm?, approval_hash?)`
200
222
  - `li_set_scheduled_post_first_comment(id, first_comment, confirm)`
201
223
  - `li_save_post_draft(text, mentions?, attachments?, first_comment?, source_key?, metadata?)`
202
224
  - `li_post_drafts()`
@@ -205,11 +227,16 @@ SignalDash exposes:
205
227
  - `li_post_draft_preview(id)`
206
228
  - `li_scheduled_posts()`
207
229
  - `li_scheduled_post_preview(id)`
230
+ - `li_scheduled_post_attachment(id, index)`
208
231
  - `li_edit_scheduled_post(id, text, scheduled_at, mentions?, attachments?, first_comment?, confirm?, approval_hash?, expected_version?, expected_updated_at?)`
209
232
  - `li_cancel_scheduled_post(id, confirm)`
210
233
  - `sd_schedule_message(channel, chat_id, text, scheduled_at, confirm)`
211
234
  - `sd_scheduled_messages(state?, channel?)`
212
235
  - `sd_cancel_scheduled_message(id, confirm)`
236
+ - `sd_followup_create(channel, thread_id, context, due_at, completion_condition?, auto_close_on_reply?, recipient_label?, origin?, source_message_id?, baseline_at?)`
237
+ - `sd_followups(status?, channel?, overdue?)`
238
+ - `sd_followup_cancel(id, confirm)`
239
+ - `sd_followup_complete(id, confirm)`
213
240
 
214
241
  Every operation runs through the hosted SignalDash backend. Agents never
215
242
  receive the Unipile access key.
@@ -271,13 +298,13 @@ whatever the page's completeness, because presence on an incomplete page is
271
298
  still presence. `replied_by_me:false` is emitted only when the reply set is
272
299
  provably whole, every reply carries a resolvable `author_details.id`, ids are
273
300
  unique, every reply belongs to this thread, and no reply is the account
274
- owner's. Two proofs are accepted: the provider reporting the reply page
275
- complete, or a zero `reply_counter` together with a reply read that came back
276
- empty, which is the same pair the reply preflight already requires.
277
- `replies_read === reply_counter` is deliberately NOT a proof; both numbers are
278
- returned so a caller can see the corroboration, but SignalDash will not convert
279
- it into a boolean, because a wrong `false` makes an agent talk over the account
280
- owner in public. Everything else is `replied_by_me:null` with an explicit
301
+ owner's. Three proofs are accepted: the provider reporting the reply page
302
+ complete; a zero `reply_counter` together with a reply read that came back
303
+ empty; or a single cursorless reply page, shorter than the requested limit,
304
+ holding at least `reply_counter` replies (`reply_counter_met_on_short_page`).
305
+ The reply and like preflights accept the same three. A cursor, a full page, a
306
+ missing counter, or fewer replies read than the counter claims stays unproven,
307
+ because a wrong `false` makes an agent talk over the account owner in public. Everything else is `replied_by_me:null` with an explicit
281
308
  reason. The flag is off by default and unresolved comments still carry
282
309
  `reply_state.state = "unknown"`, so a miss is never mistaken for a proven
283
310
  absence. Resolution is bounded per request by
@@ -379,11 +406,50 @@ comment have human approval. The stored image bytes and comment text cannot
379
406
  drift after approval. The scheduler publishes the comment through the same
380
407
  connected LinkedIn account immediately after the post. It persists the post ID
381
408
  before attempting the comment, so a comment failure never republishes the post.
409
+ On the `li_draft_post` create path, every future post with `attachments` and no
410
+ `content_pipeline_override` must first preview the exact payload without
411
+ `confirm`, then repeat it with `publish:true`, `confirm:true`, and that preview's
412
+ single-use `approval_hash`.
382
413
  The scheduler
383
414
  uses the same sender binding, action ledger, duplicate guard, daily budget, and
384
415
  provider-warning lock as immediate publishing. An interrupted or ambiguous
385
416
  execution fails closed and is never retried automatically.
386
417
 
418
+ A post carries either images or one video, never both. An image is still
419
+ carried as `content_base64` inside the request body. A video is carried as
420
+ `content_url`: one absolute https URL, at most 64 MiB, whose bytes are streamed
421
+ once at publish time and never stored. 31 MB of mp4 is ~42 MB of base64, which
422
+ would have to survive a JSON parse and then live in the `attachments_json` cell
423
+ that is re-read on every list of the queue, so storing the URL is the point
424
+ rather than a convenience. LinkedIn's own feed-video limit is 200 MB; the lower
425
+ ceiling here is because the multipart encoder buffers the body and then
426
+ concatenates it, so a publish holds roughly twice the video in memory.
427
+
428
+ That fetch is fenced, because a server that follows a URL its caller supplied
429
+ is otherwise an SSRF hole: https only; every resolved address checked against
430
+ the private, loopback, link-local, unique-local and reserved ranges in IPv4 and
431
+ IPv6, with the connection pinned to the address that passed so a second
432
+ resolution cannot substitute a private one; every redirect re-running the whole
433
+ check under a hop cap; the ceiling enforced mid-stream rather than from a
434
+ Content-Length the same server supplied; one deadline across the whole
435
+ operation; and an arriving Content-Type that must equal the declared one. The
436
+ fetch runs before the provider write, so a URL that has gone dead costs no
437
+ action budget, no idempotency record and no sender lock: the row lands on
438
+ `failed` carrying the exact refusal code.
439
+
440
+ Two things the fence alone does not give. The arriving `Content-Type` is a
441
+ claim by the server being distrusted, so the bytes must additionally open with
442
+ an ISO `ftyp` box or the publish fails `media_not_a_video`. And because the
443
+ bytes are fetched later, approving a video approves a URL: whoever controls
444
+ that host decides what reaches LinkedIn between the approval and the scheduled
445
+ minute. An attachment may therefore carry `content_sha256`, the exact lowercase
446
+ hex SHA-256 of the file; the fetched bytes are hashed and compared before
447
+ anything is handed to the multipart encoder, and a mismatch fails the row
448
+ `media_digest_mismatch`. It is bound into the approval hash alongside the URL,
449
+ so an approval never covers a different video. Without it the guarantee is only
450
+ that the URL did not change after approval, which is why the review page shows
451
+ the URL itself rather than a label.
452
+
387
453
  Arbitrary saved LinkedIn drafts use a separate durable store and never enter
388
454
  the scheduling or publishing worker. `li_save_post_draft` stores exact text,
389
455
  up to 25 mentions, up to 20 ordered images, and an optional first comment.
@@ -491,6 +557,17 @@ per-client throttle. Already-issued 24-character compact tokens and 48-character
491
557
  `/scheduled-post/h/<token>` or `/calendar/h/<token>` links remain valid until
492
558
  their normal expiry.
493
559
 
560
+ `li_scheduled_post_attachment` is the agent-side path to the same stored bytes
561
+ the preview page renders, without a browser session: it returns one attachment
562
+ of one active post with its `filename` and its stored `content_type` verbatim,
563
+ as `content_base64` for an image or as `content_url` for a video whose bytes
564
+ are never stored here. It never fetches a URL on a caller behalf. It is scoped by the same `data_ref` and the same
565
+ draft/scheduled/executing filter as `li_scheduled_post_preview`, so a cancelled
566
+ or published post, a foreign post, and a missing index are one indistinguishable
567
+ 404. The bytes are deliberately not part of the preview response, which reports
568
+ only `attachment_count`, because one image is easily hundreds of kilobytes of
569
+ base64 and the preview is usually read for the text and the time.
570
+
494
571
  The shared calendar preserves validated Buffer LinkedIn tag ranges and URLs in
495
572
  the post body and also keeps the separate tag audit pills. Valid provider
496
573
  annotations render as clickable LinkedIn links in both places. SignalDash
@@ -730,6 +807,17 @@ untracked files, and submodule state present when the process started.
730
807
  service restart. It defaults to enabled and exposes no credential, account, or
731
808
  message data.
732
809
 
810
+ The sanctioned WhatsApp sender is tracked at `ops/signaldash-wa-send`. Install
811
+ that exact reviewed helper with:
812
+
813
+ ```bash
814
+ sudo install -m 0755 ops/signaldash-wa-send /usr/local/bin/signaldash-wa-send
815
+ ```
816
+
817
+ The helper rejects Unicode U+2014 locally. The server repeats the same check at
818
+ the authoritative write boundary so older clients and alternate callers cannot
819
+ bypass it.
820
+
733
821
  ## Releasing
734
822
 
735
823
  A release is a tag push. `.github/workflows/release.yml` runs `./release.sh` on