@floomhq/signaldash 0.39.19 → 0.41.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.19 <invite-code>
123
+ npx -y @floomhq/signaldash@0.41.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,8 @@ 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_delete_post(post_id, expected_text_prefix, confirm)` (own post only, bound account only, capability `linkedin_post_delete`)
222
+ - `li_draft_post(text, publish, scheduled_at?, content_pipeline_id?, content_pipeline_override?, mentions?, attachments?, first_comment?, confirm?, approval_hash?)`
200
223
  - `li_set_scheduled_post_first_comment(id, first_comment, confirm)`
201
224
  - `li_save_post_draft(text, mentions?, attachments?, first_comment?, source_key?, metadata?)`
202
225
  - `li_post_drafts()`
@@ -211,6 +234,10 @@ SignalDash exposes:
211
234
  - `sd_schedule_message(channel, chat_id, text, scheduled_at, confirm)`
212
235
  - `sd_scheduled_messages(state?, channel?)`
213
236
  - `sd_cancel_scheduled_message(id, confirm)`
237
+ - `sd_followup_create(channel, thread_id, context, due_at, completion_condition?, auto_close_on_reply?, recipient_label?, origin?, source_message_id?, baseline_at?)`
238
+ - `sd_followups(status?, channel?, overdue?)`
239
+ - `sd_followup_cancel(id, confirm)`
240
+ - `sd_followup_complete(id, confirm)`
214
241
 
215
242
  Every operation runs through the hosted SignalDash backend. Agents never
216
243
  receive the Unipile access key.
@@ -272,13 +299,13 @@ whatever the page's completeness, because presence on an incomplete page is
272
299
  still presence. `replied_by_me:false` is emitted only when the reply set is
273
300
  provably whole, every reply carries a resolvable `author_details.id`, ids are
274
301
  unique, every reply belongs to this thread, and no reply is the account
275
- owner's. Two proofs are accepted: the provider reporting the reply page
276
- complete, or a zero `reply_counter` together with a reply read that came back
277
- empty, which is the same pair the reply preflight already requires.
278
- `replies_read === reply_counter` is deliberately NOT a proof; both numbers are
279
- returned so a caller can see the corroboration, but SignalDash will not convert
280
- it into a boolean, because a wrong `false` makes an agent talk over the account
281
- owner in public. Everything else is `replied_by_me:null` with an explicit
302
+ owner's. Three proofs are accepted: the provider reporting the reply page
303
+ complete; a zero `reply_counter` together with a reply read that came back
304
+ empty; or a single cursorless reply page, shorter than the requested limit,
305
+ holding at least `reply_counter` replies (`reply_counter_met_on_short_page`).
306
+ The reply and like preflights accept the same three. A cursor, a full page, a
307
+ missing counter, or fewer replies read than the counter claims stays unproven,
308
+ because a wrong `false` makes an agent talk over the account owner in public. Everything else is `replied_by_me:null` with an explicit
282
309
  reason. The flag is off by default and unresolved comments still carry
283
310
  `reply_state.state = "unknown"`, so a miss is never mistaken for a proven
284
311
  absence. Resolution is bounded per request by
@@ -380,11 +407,50 @@ comment have human approval. The stored image bytes and comment text cannot
380
407
  drift after approval. The scheduler publishes the comment through the same
381
408
  connected LinkedIn account immediately after the post. It persists the post ID
382
409
  before attempting the comment, so a comment failure never republishes the post.
410
+ On the `li_draft_post` create path, every future post with `attachments` and no
411
+ `content_pipeline_override` must first preview the exact payload without
412
+ `confirm`, then repeat it with `publish:true`, `confirm:true`, and that preview's
413
+ single-use `approval_hash`.
383
414
  The scheduler
384
415
  uses the same sender binding, action ledger, duplicate guard, daily budget, and
385
416
  provider-warning lock as immediate publishing. An interrupted or ambiguous
386
417
  execution fails closed and is never retried automatically.
387
418
 
419
+ A post carries either images or one video, never both. An image is still
420
+ carried as `content_base64` inside the request body. A video is carried as
421
+ `content_url`: one absolute https URL, at most 64 MiB, whose bytes are streamed
422
+ once at publish time and never stored. 31 MB of mp4 is ~42 MB of base64, which
423
+ would have to survive a JSON parse and then live in the `attachments_json` cell
424
+ that is re-read on every list of the queue, so storing the URL is the point
425
+ rather than a convenience. LinkedIn's own feed-video limit is 200 MB; the lower
426
+ ceiling here is because the multipart encoder buffers the body and then
427
+ concatenates it, so a publish holds roughly twice the video in memory.
428
+
429
+ That fetch is fenced, because a server that follows a URL its caller supplied
430
+ is otherwise an SSRF hole: https only; every resolved address checked against
431
+ the private, loopback, link-local, unique-local and reserved ranges in IPv4 and
432
+ IPv6, with the connection pinned to the address that passed so a second
433
+ resolution cannot substitute a private one; every redirect re-running the whole
434
+ check under a hop cap; the ceiling enforced mid-stream rather than from a
435
+ Content-Length the same server supplied; one deadline across the whole
436
+ operation; and an arriving Content-Type that must equal the declared one. The
437
+ fetch runs before the provider write, so a URL that has gone dead costs no
438
+ action budget, no idempotency record and no sender lock: the row lands on
439
+ `failed` carrying the exact refusal code.
440
+
441
+ Two things the fence alone does not give. The arriving `Content-Type` is a
442
+ claim by the server being distrusted, so the bytes must additionally open with
443
+ an ISO `ftyp` box or the publish fails `media_not_a_video`. And because the
444
+ bytes are fetched later, approving a video approves a URL: whoever controls
445
+ that host decides what reaches LinkedIn between the approval and the scheduled
446
+ minute. An attachment may therefore carry `content_sha256`, the exact lowercase
447
+ hex SHA-256 of the file; the fetched bytes are hashed and compared before
448
+ anything is handed to the multipart encoder, and a mismatch fails the row
449
+ `media_digest_mismatch`. It is bound into the approval hash alongside the URL,
450
+ so an approval never covers a different video. Without it the guarantee is only
451
+ that the URL did not change after approval, which is why the review page shows
452
+ the URL itself rather than a label.
453
+
388
454
  Arbitrary saved LinkedIn drafts use a separate durable store and never enter
389
455
  the scheduling or publishing worker. `li_save_post_draft` stores exact text,
390
456
  up to 25 mentions, up to 20 ordered images, and an optional first comment.
@@ -494,8 +560,9 @@ their normal expiry.
494
560
 
495
561
  `li_scheduled_post_attachment` is the agent-side path to the same stored bytes
496
562
  the preview page renders, without a browser session: it returns one attachment
497
- of one active post as `content_base64` with its `filename` and its stored
498
- `content_type` verbatim. It is scoped by the same `data_ref` and the same
563
+ of one active post with its `filename` and its stored `content_type` verbatim,
564
+ as `content_base64` for an image or as `content_url` for a video whose bytes
565
+ are never stored here. It never fetches a URL on a caller behalf. It is scoped by the same `data_ref` and the same
499
566
  draft/scheduled/executing filter as `li_scheduled_post_preview`, so a cancelled
500
567
  or published post, a foreign post, and a missing index are one indistinguishable
501
568
  404. The bytes are deliberately not part of the preview response, which reports
@@ -741,6 +808,17 @@ untracked files, and submodule state present when the process started.
741
808
  service restart. It defaults to enabled and exposes no credential, account, or
742
809
  message data.
743
810
 
811
+ The sanctioned WhatsApp sender is tracked at `ops/signaldash-wa-send`. Install
812
+ that exact reviewed helper with:
813
+
814
+ ```bash
815
+ sudo install -m 0755 ops/signaldash-wa-send /usr/local/bin/signaldash-wa-send
816
+ ```
817
+
818
+ The helper rejects Unicode U+2014 locally. The server repeats the same check at
819
+ the authoritative write boundary so older clients and alternate callers cannot
820
+ bypass it.
821
+
744
822
  ## Releasing
745
823
 
746
824
  A release is a tag push. `.github/workflows/release.yml` runs `./release.sh` on