@floomhq/signaldash 0.40.0 → 0.42.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 +35 -1
- package/bin/sd.mjs +146 -8
- package/package.json +2 -2
- package/skills/signaldash/SKILL.md +53 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ the human to complete. The equivalent manual sequence is:
|
|
|
23
23
|
npx -y @floomhq/signaldash login <invite-code>
|
|
24
24
|
npx -y @floomhq/signaldash connect linkedin
|
|
25
25
|
npx -y @floomhq/signaldash connect whatsapp
|
|
26
|
+
npx -y @floomhq/signaldash connect instagram # optional
|
|
26
27
|
npx -y @floomhq/signaldash connect email
|
|
27
28
|
```
|
|
28
29
|
|
|
@@ -120,7 +121,7 @@ setup installs both from the same pinned npm package
|
|
|
120
121
|
the human chose to execute:
|
|
121
122
|
|
|
122
123
|
```bash
|
|
123
|
-
npx -y @floomhq/signaldash@0.
|
|
124
|
+
npx -y @floomhq/signaldash@0.42.0 <invite-code>
|
|
124
125
|
```
|
|
125
126
|
|
|
126
127
|
Run that command in a terminal, not in an agent chat. Do not ask an agent to
|
|
@@ -208,6 +209,11 @@ timer, scheduler entry, Buffer item, or provider write.
|
|
|
208
209
|
- `wa_send_message(chat_id, text?, attachments?, expected_watermark?, mark_read?)` — confirmed sends clear unread by default; `mark_read:false` preserves unread.
|
|
209
210
|
- `wa_delete_message(chat_id, message_id)`
|
|
210
211
|
- `wa_delete_messages(messages)`
|
|
212
|
+
- `ig_list_chats(limit?, cursor?, search?, max_scan?, unread?)`
|
|
213
|
+
- `ig_read_messages(chat_id, limit?)`
|
|
214
|
+
- `ig_mark_read(chat_id)`
|
|
215
|
+
- `ig_get_attachment(chat_id, message_id, attachment_id)`
|
|
216
|
+
- `ig_send_message(chat_id, text, expected_watermark?, mark_read?)`: text only, same read-before-send guards as WhatsApp, Instagram budget below.
|
|
211
217
|
- `email_list(limit)`
|
|
212
218
|
- `email_read(thread_id, limit)`
|
|
213
219
|
- `email_send(to, subject, body)`
|
|
@@ -218,6 +224,7 @@ timer, scheduler entry, Buffer item, or provider write.
|
|
|
218
224
|
- `li_like_comment(post_id, parent_comment_id, comment_id, expected_watermark?)`
|
|
219
225
|
- `li_delete_message(chat_id, message_id, confirm)`
|
|
220
226
|
- `li_delete_comment(post_id, comment_id, confirm)`
|
|
227
|
+
- `li_delete_post(post_id, expected_text_prefix, confirm)` (own post only, bound account only, capability `linkedin_post_delete`)
|
|
221
228
|
- `li_draft_post(text, publish, scheduled_at?, content_pipeline_id?, content_pipeline_override?, mentions?, attachments?, first_comment?, confirm?, approval_hash?)`
|
|
222
229
|
- `li_set_scheduled_post_first_comment(id, first_comment, confirm)`
|
|
223
230
|
- `li_save_post_draft(text, mentions?, attachments?, first_comment?, source_key?, metadata?)`
|
|
@@ -727,6 +734,33 @@ workflow around that runtime control:
|
|
|
727
734
|
the conservative reading is the safe one) and clears at the next UTC midnight;
|
|
728
735
|
LinkedIn, WhatsApp/email, and delete ledgers are keyed to the stable logical
|
|
729
736
|
user or sender rather than multiplied by live bearer sessions.
|
|
737
|
+
- Instagram DMs have their own budget per connected Instagram account, never
|
|
738
|
+
shared with LinkedIn or with the WhatsApp/email lane. Defaults: 50 sends per
|
|
739
|
+
UTC day, of which at most 15 may open a new conversation; 250 sends and 60
|
|
740
|
+
new conversations per UTC week (Monday 00:00 UTC); 10 sends per rolling hour;
|
|
741
|
+
at least 45 seconds between sends plus 0 to 30 seconds of random jitter. A
|
|
742
|
+
send counts as a new conversation when the page of the thread that was read
|
|
743
|
+
holds no message from the account itself (a cold DM, or a first answer to a
|
|
744
|
+
message request); every other send is a reply. A freshly connected account
|
|
745
|
+
warms up over 14 days: the first 24 hours allow 10 replies and no new
|
|
746
|
+
conversation, days 1 to 6 allow 20 sends and 5 new conversations, days 7 to
|
|
747
|
+
13 allow 35 sends and 10 new conversations. A provider 403 or 429 on a send
|
|
748
|
+
pauses Instagram sends for that account for 24 hours. `sd_budget_status`
|
|
749
|
+
reports all of it under `instagram`. Per-account overrides go in
|
|
750
|
+
`SIGNALDASH_INSTAGRAM_BUDGET_OVERRIDES`, a JSON object keyed by the Unipile
|
|
751
|
+
account id with any of `daily_total`, `new_chats_daily`, `weekly_total`,
|
|
752
|
+
`new_chats_weekly`, `hourly_total`, `min_gap_ms`, `gap_jitter_ms` and
|
|
753
|
+
`warmup` (boolean). Overrides can raise or lower a limit but never past the
|
|
754
|
+
hard ceilings of 100 per day, 10 per hour, 40 new conversations per day, 500
|
|
755
|
+
per week, 200 new conversations per week, and a 20 second minimum gap.
|
|
756
|
+
Sources for the numbers: Unipile provider limits (at most 100 actions per day
|
|
757
|
+
and 10 per hour per Instagram account, start new accounts lower,
|
|
758
|
+
https://developer.unipile.com/docs/provider-limits-and-restrictions), Wave
|
|
759
|
+
(50 DMs per day including follow-ups as the common ceiling, 10 per day for a
|
|
760
|
+
new account, https://www.usewave.co/blog/instagram-dm-limits) and Celestia
|
|
761
|
+
Leads (20 to 35 cold DMs per day once warmed, under 160 per week, pause 24 to
|
|
762
|
+
72 hours after an action block,
|
|
763
|
+
https://celestialeads.com/blog/instagram-dm-automation-guide).
|
|
730
764
|
- Read the exact thread before every send.
|
|
731
765
|
- Preview an exact invitation target and note before sending it.
|
|
732
766
|
- List the exact received or sent invitation before accepting or withdrawing it.
|
package/bin/sd.mjs
CHANGED
|
@@ -278,8 +278,8 @@ export async function cmdConnect(provider, dependencies = {}) {
|
|
|
278
278
|
const now = dependencies.now || Date.now;
|
|
279
279
|
const log = dependencies.log || console.log;
|
|
280
280
|
const error = dependencies.error || console.error;
|
|
281
|
-
if (!["linkedin", "whatsapp", "email"].includes(provider)) {
|
|
282
|
-
error("usage: signaldash connect linkedin|whatsapp|email");
|
|
281
|
+
if (!["linkedin", "whatsapp", "instagram", "email"].includes(provider)) {
|
|
282
|
+
error("usage: signaldash connect linkedin|whatsapp|instagram|email");
|
|
283
283
|
process.exitCode = 1;
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
@@ -864,12 +864,14 @@ const TOOLS = [
|
|
|
864
864
|
{
|
|
865
865
|
name: "li_send_invitation",
|
|
866
866
|
path: "/li/send_invitation",
|
|
867
|
-
description: "Preview or send one exact LinkedIn invitation. First call without confirm, obtain approval, then repeat the exact provider_id and
|
|
867
|
+
description: "Preview or send one exact LinkedIn invitation. First call without confirm, obtain approval, then repeat the exact provider_id, note, follow_up and note_mode with confirm:true. DEFAULT: a note is NOT put on the invitation (free accounts get notes refused); the invitation goes out bare and the note text is sent once as the first message after the person is proven to have accepted. Set note_mode \"inline\" only when the note must ride on the invitation itself. The preview shows the exact follow-up text; list or cancel pending follow-ups with sd_invitation_followups / sd_invitation_followup_cancel.",
|
|
868
868
|
inputSchema: {
|
|
869
869
|
type: "object",
|
|
870
870
|
properties: {
|
|
871
871
|
provider_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
872
|
-
note: { type: "string", maxLength: 300 },
|
|
872
|
+
note: { type: "string", maxLength: 300, description: "With the default note_mode follow_up this text becomes the after-acceptance message; with note_mode inline it rides on the invitation." },
|
|
873
|
+
follow_up: { type: "string", minLength: 1, maxLength: 5000, description: "Exact message to send once after acceptance. Use instead of note (default mode), or alongside an inline note." },
|
|
874
|
+
note_mode: { type: "string", enum: ["follow_up", "inline"], default: "follow_up" },
|
|
873
875
|
confirm: { type: "boolean", default: false },
|
|
874
876
|
},
|
|
875
877
|
required: ["provider_id"],
|
|
@@ -939,7 +941,7 @@ const TOOLS = [
|
|
|
939
941
|
properties: {
|
|
940
942
|
channel: {
|
|
941
943
|
type: "string",
|
|
942
|
-
enum: ["linkedin", "whatsapp", "email"],
|
|
944
|
+
enum: ["linkedin", "whatsapp", "instagram", "email"],
|
|
943
945
|
},
|
|
944
946
|
identifiers: {
|
|
945
947
|
type: "array",
|
|
@@ -990,7 +992,7 @@ const TOOLS = [
|
|
|
990
992
|
{
|
|
991
993
|
name: "sd_budget_status",
|
|
992
994
|
path: "/sd/budget_status",
|
|
993
|
-
description: "Read authoritative SignalDash LinkedIn attempt capacity for the invitation, existing-thread message, and own-post engagement daily lanes, the aggregate emergency brake, and the unchanged weekly invitation cap.",
|
|
995
|
+
description: "Read authoritative SignalDash LinkedIn attempt capacity for the invitation, existing-thread message, and own-post engagement daily lanes, the aggregate emergency brake, and the unchanged weekly invitation cap. When Instagram is connected, `instagram` reports that account's own DM budget: daily, weekly and hourly totals, the separate new-conversation caps, the warm-up stage, the next allowed send time and any provider pause.",
|
|
994
996
|
inputSchema: {
|
|
995
997
|
type: "object",
|
|
996
998
|
properties: {},
|
|
@@ -1677,6 +1679,85 @@ const TOOLS = [
|
|
|
1677
1679
|
additionalProperties: false,
|
|
1678
1680
|
},
|
|
1679
1681
|
},
|
|
1682
|
+
{
|
|
1683
|
+
name: "ig_list_chats",
|
|
1684
|
+
ch: "ig",
|
|
1685
|
+
action: "list_chats",
|
|
1686
|
+
description: "List or search the Instagram DM chats of the Instagram account bound to you. `unread:true` asks the provider for unread chats only. A search filters over a bounded scan of chat pages and reports `scanned_chats` and `exhaustive`, so a miss is never a proven absence.",
|
|
1687
|
+
inputSchema: {
|
|
1688
|
+
type: "object",
|
|
1689
|
+
properties: {
|
|
1690
|
+
limit: { type: "integer", minimum: 1, maximum: 100, description: "How many chats to return. With `search`, how many MATCHES to return." },
|
|
1691
|
+
cursor: { type: "string", maxLength: 4000, description: "Continue from a previous page or a previous search." },
|
|
1692
|
+
search: { type: "string", minLength: 1, maxLength: 200, description: "Filter chats case-insensitively on names, usernames and counterpart identifiers." },
|
|
1693
|
+
max_scan: { type: "integer", minimum: 1, maximum: 500, description: "How many chats the search may scan before it stops and reports exhaustive:false. Default 200." },
|
|
1694
|
+
unread: { type: "boolean", description: "Only unread chats." },
|
|
1695
|
+
},
|
|
1696
|
+
additionalProperties: false,
|
|
1697
|
+
},
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
name: "ig_read_messages",
|
|
1701
|
+
ch: "ig",
|
|
1702
|
+
action: "read",
|
|
1703
|
+
description: "Read messages and the resolved chat in one Instagram DM thread of your bound Instagram account. Returns current_watermark for a later ig_send_message. Reading never marks the chat read. args: chat_id",
|
|
1704
|
+
inputSchema: {
|
|
1705
|
+
type: "object",
|
|
1706
|
+
properties: {
|
|
1707
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1708
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
1709
|
+
},
|
|
1710
|
+
required: ["chat_id"],
|
|
1711
|
+
additionalProperties: false,
|
|
1712
|
+
},
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
name: "ig_mark_read",
|
|
1716
|
+
ch: "ig",
|
|
1717
|
+
action: "mark_read",
|
|
1718
|
+
description: "Explicitly clear the unread state of one Instagram DM chat of your bound account. Reading a thread never clears it automatically.",
|
|
1719
|
+
inputSchema: {
|
|
1720
|
+
type: "object",
|
|
1721
|
+
properties: {
|
|
1722
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1723
|
+
},
|
|
1724
|
+
required: ["chat_id"],
|
|
1725
|
+
additionalProperties: false,
|
|
1726
|
+
},
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
name: "ig_get_attachment",
|
|
1730
|
+
path: "/ig/get_attachment",
|
|
1731
|
+
description: "Download one attachment of one Instagram DM message in a chat your bound Instagram account owns and store it on the SignalDash host. Read the chat first to obtain the exact message_id and attachment_id. Returns the stored path, mimetype, byte size and sha256.",
|
|
1732
|
+
inputSchema: {
|
|
1733
|
+
type: "object",
|
|
1734
|
+
properties: {
|
|
1735
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1736
|
+
message_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1737
|
+
attachment_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1738
|
+
},
|
|
1739
|
+
required: ["chat_id", "message_id", "attachment_id"],
|
|
1740
|
+
additionalProperties: false,
|
|
1741
|
+
},
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
name: "ig_send_message",
|
|
1745
|
+
ch: "ig",
|
|
1746
|
+
action: "send",
|
|
1747
|
+
description: "Send one Instagram DM (text only) in an existing chat of your bound Instagram account. Same guards as wa_send_message: read the chat first with ig_read_messages, a changed thread returns 409 thread_changed, an identical message is refused, and an unconfirmed send blocks its retry until you re-read and pass confirm_resend:true. Instagram has its own conservative per-account budget: a send into a thread with no message of yours on the page you read counts as a NEW conversation (much smaller cap, none at all in the first 24 hours after connecting), every other send is a reply; daily, weekly and hourly totals apply to both, sends are spaced at least 45 seconds apart plus random jitter, a freshly connected account warms up over 14 days, and a provider 403/429 pauses Instagram sends for 24 hours. A refusal names the exact cap and when it frees up; check sd_budget_status first. args: chat_id, text, expected_watermark, mark_read",
|
|
1748
|
+
inputSchema: {
|
|
1749
|
+
type: "object",
|
|
1750
|
+
properties: {
|
|
1751
|
+
chat_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1752
|
+
text: { type: "string", minLength: 1, maxLength: 5000 },
|
|
1753
|
+
confirm_resend: { type: "boolean", const: true },
|
|
1754
|
+
expected_watermark: { type: "string", minLength: 64, maxLength: 64, pattern: "^[0-9a-f]{64}$", description: "Bind the send to the exact current_watermark returned by the read or a thread_changed response." },
|
|
1755
|
+
mark_read: { type: "boolean", default: true, description: "Clear unread after confirmed success; false preserves unread state." },
|
|
1756
|
+
},
|
|
1757
|
+
required: ["chat_id", "text"],
|
|
1758
|
+
additionalProperties: false,
|
|
1759
|
+
},
|
|
1760
|
+
},
|
|
1680
1761
|
{
|
|
1681
1762
|
name: "email_list",
|
|
1682
1763
|
path: "/email/list",
|
|
@@ -1859,6 +1940,21 @@ const TOOLS = [
|
|
|
1859
1940
|
additionalProperties: false,
|
|
1860
1941
|
},
|
|
1861
1942
|
},
|
|
1943
|
+
{
|
|
1944
|
+
name: "li_delete_post",
|
|
1945
|
+
path: "/li/delete_post",
|
|
1946
|
+
description: "Delete ONE LinkedIn post authored by this authenticated account. Irreversible; disabled unless the operator enables the linkedin_post_delete capability. Acts only as the account bound to this SignalDash session. Requires the exact social_id from li_my_posts (urn:li:activity:<digits>), expected_text_prefix (the first 10-500 characters of the post text, exactly) and confirm:true. Before deleting, SignalDash reads the post with the same account and refuses unless the author is this sender (post_not_own), it is not a repost, and the text starts with expected_text_prefix (text_prefix_mismatch). Returns the post evidence and a post-delete readback; a replay of the same post is refused as duplicate_delete. Deletion is remediation, not rollback.",
|
|
1947
|
+
inputSchema: {
|
|
1948
|
+
type: "object",
|
|
1949
|
+
properties: {
|
|
1950
|
+
post_id: { type: "string", pattern: "^urn:li:activity:[0-9]{1,30}$", maxLength: 60 },
|
|
1951
|
+
expected_text_prefix: { type: "string", minLength: 10, maxLength: 500 },
|
|
1952
|
+
confirm: { type: "boolean", const: true },
|
|
1953
|
+
},
|
|
1954
|
+
required: ["post_id", "expected_text_prefix", "confirm"],
|
|
1955
|
+
additionalProperties: false,
|
|
1956
|
+
},
|
|
1957
|
+
},
|
|
1862
1958
|
{
|
|
1863
1959
|
name: "li_draft_post",
|
|
1864
1960
|
path: "/li/create_post",
|
|
@@ -2325,6 +2421,48 @@ const TOOLS = [
|
|
|
2325
2421
|
additionalProperties: false,
|
|
2326
2422
|
},
|
|
2327
2423
|
},
|
|
2424
|
+
{
|
|
2425
|
+
name: "sd_invitation_followups",
|
|
2426
|
+
path: "/sd/invitation_followups",
|
|
2427
|
+
description: "List this user's invitation follow-ups: exact messages that are sent once a LinkedIn invitation is proven accepted. States: pending (waiting for acceptance, or accepted and waiting for a send slot), sending, sent, cancelled, expired (not accepted within 30 days), needs_review (the thread moved, e.g. they wrote first, or the outcome is unknown; never retried automatically).",
|
|
2428
|
+
inputSchema: {
|
|
2429
|
+
type: "object",
|
|
2430
|
+
properties: {
|
|
2431
|
+
state: { type: "string", enum: ["pending", "sending", "sent", "cancelled", "expired", "needs_review"] },
|
|
2432
|
+
},
|
|
2433
|
+
additionalProperties: false,
|
|
2434
|
+
},
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
name: "sd_invitation_followup_create",
|
|
2438
|
+
path: "/sd/invitation_followup_create",
|
|
2439
|
+
description: "Register an after-acceptance follow-up message for an invitation that was ALREADY sent. First call without confirm: SignalDash proves a pending sent invitation to this exact provider_id exists and shows the exact text. After the human approves, repeat the same provider_id, text and invitation_id with confirm:true.",
|
|
2440
|
+
inputSchema: {
|
|
2441
|
+
type: "object",
|
|
2442
|
+
properties: {
|
|
2443
|
+
provider_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
2444
|
+
text: { type: "string", minLength: 1, maxLength: 5000 },
|
|
2445
|
+
invitation_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
2446
|
+
confirm: { type: "boolean", default: false },
|
|
2447
|
+
},
|
|
2448
|
+
required: ["provider_id", "text"],
|
|
2449
|
+
additionalProperties: false,
|
|
2450
|
+
},
|
|
2451
|
+
},
|
|
2452
|
+
{
|
|
2453
|
+
name: "sd_invitation_followup_cancel",
|
|
2454
|
+
path: "/sd/invitation_followup_cancel",
|
|
2455
|
+
description: "Cancel one exact invitation follow-up while it is still pending. Requires id and confirm:true. It cannot stop or unsend one already being sent or sent.",
|
|
2456
|
+
inputSchema: {
|
|
2457
|
+
type: "object",
|
|
2458
|
+
properties: {
|
|
2459
|
+
id: { type: "string", format: "uuid" },
|
|
2460
|
+
confirm: { type: "boolean", const: true },
|
|
2461
|
+
},
|
|
2462
|
+
required: ["id", "confirm"],
|
|
2463
|
+
additionalProperties: false,
|
|
2464
|
+
},
|
|
2465
|
+
},
|
|
2328
2466
|
{
|
|
2329
2467
|
name: "sd_followup_create",
|
|
2330
2468
|
path: "/sd/followup_create",
|
|
@@ -2587,7 +2725,7 @@ export async function cmdStatus(dependencies = {}) {
|
|
|
2587
2725
|
const request = dependencies.request || api;
|
|
2588
2726
|
log("");
|
|
2589
2727
|
log(" " + chalk.bold("SignalDash") + chalk.dim(" " + (cfg.backend || DEFAULT_BACKEND)));
|
|
2590
|
-
for (const provider of ["linkedin", "whatsapp", "email"]) {
|
|
2728
|
+
for (const provider of ["linkedin", "whatsapp", "instagram", "email"]) {
|
|
2591
2729
|
const r = await request(`/connect/${provider}/status`, undefined, { method: "GET" });
|
|
2592
2730
|
const ok = r.status === 200 && r.json.connected;
|
|
2593
2731
|
const unavailable = r.json?.code === "upstream_authentication_failed"
|
|
@@ -2726,7 +2864,7 @@ function printHelp(log = console.log) {
|
|
|
2726
2864
|
signaldash <invite-code> set up everything in one go
|
|
2727
2865
|
signaldash login-token <token> store a session token you were given
|
|
2728
2866
|
signaldash status show what is connected
|
|
2729
|
-
signaldash connect linkedin|whatsapp|email
|
|
2867
|
+
signaldash connect linkedin|whatsapp|instagram|email
|
|
2730
2868
|
signaldash connections [file.csv] export your LinkedIn connections
|
|
2731
2869
|
signaldash skill install the agent skill
|
|
2732
2870
|
signaldash mcp run the MCP server (used by your agent)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floomhq/signaldash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "Secure LinkedIn, WhatsApp, and email access for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "node --test",
|
|
22
|
-
"check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check server/outbound-text-policy.cjs && node --check server/intro-request-notifier.cjs && node --check server/content-pipeline.cjs && node --check server/x-signal-engine.cjs && node --check server/buffer-calendar.cjs && node --check server/composio-email.cjs && node --check server/attachments.cjs && node --check server/media-fetch.cjs && node --check server/write-control.cjs && node --check server/secretary-store.cjs && node --check server/secretary-collector.cjs && node --check server/secretary-latest.cjs && node --check server/secretary-approvals.cjs && node --check server/secretary-push.cjs && node --check server/secretary-rules.cjs && node --check server/secretary-shadow-gate.cjs && node --check server/secretary-vault-importer.cjs && node --check server/secretary-inspiration.cjs && node --check server/linkedin-archive-importer.cjs && node --check server/voice-profile.cjs && node --check server/campaign-store.cjs && node --check server/campaign-runner.cjs && node --check server/withdrawal-store.cjs && node --check server/post-scheduler.cjs && node --check server/draft-rewrite-worker.cjs && node --check server/message-scheduler.cjs && node --check server/wa-chat-index.cjs && node --check scripts/import-linkedin-archive.mjs && node --check scripts/import-secretary-vault-snapshot.mjs && node --check scripts/rebuild-secretary-actors.mjs && node --check scripts/extract-conversation-inspiration.mjs && node --check scripts/render-content-board.mjs && node --check scripts/collect-x-signals.mjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/config-file.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js && node --check lib/x-signal-collector.cjs"
|
|
22
|
+
"check": "node --check bin/sd.mjs && node --check server/server.cjs && node --check server/outbound-text-policy.cjs && node --check server/intro-request-notifier.cjs && node --check server/content-pipeline.cjs && node --check server/x-signal-engine.cjs && node --check server/buffer-calendar.cjs && node --check server/composio-email.cjs && node --check server/attachments.cjs && node --check server/media-fetch.cjs && node --check server/write-control.cjs && node --check server/secretary-store.cjs && node --check server/secretary-collector.cjs && node --check server/secretary-latest.cjs && node --check server/secretary-approvals.cjs && node --check server/secretary-push.cjs && node --check server/secretary-rules.cjs && node --check server/secretary-shadow-gate.cjs && node --check server/secretary-vault-importer.cjs && node --check server/secretary-inspiration.cjs && node --check server/linkedin-archive-importer.cjs && node --check server/voice-profile.cjs && node --check server/campaign-store.cjs && node --check server/campaign-runner.cjs && node --check server/withdrawal-store.cjs && node --check server/post-scheduler.cjs && node --check server/draft-rewrite-worker.cjs && node --check server/message-scheduler.cjs && node --check server/invitation-followups.cjs && node --check server/wa-chat-index.cjs && node --check server/instagram-budget.cjs && node --check scripts/import-linkedin-archive.mjs && node --check scripts/import-secretary-vault-snapshot.mjs && node --check scripts/rebuild-secretary-actors.mjs && node --check scripts/extract-conversation-inspiration.mjs && node --check scripts/render-content-board.mjs && node --check scripts/collect-x-signals.mjs && node --check bin/signaldash.js && node --check lib/cli.js && node --check lib/config-file.js && node --check lib/mcp.js && node --check lib/rate-guard.js && node --check lib/secrets.js && node --check lib/unipile.js && node --check lib/x-signal-collector.cjs"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
|
25
25
|
"mcp",
|
|
@@ -261,9 +261,12 @@ Run a missing provider explicitly:
|
|
|
261
261
|
```bash
|
|
262
262
|
npx -y @floomhq/signaldash connect linkedin
|
|
263
263
|
npx -y @floomhq/signaldash connect whatsapp
|
|
264
|
+
npx -y @floomhq/signaldash connect instagram
|
|
264
265
|
npx -y @floomhq/signaldash connect email
|
|
265
266
|
```
|
|
266
267
|
|
|
268
|
+
Instagram is opt-in like email: the one-command setup does not connect it.
|
|
269
|
+
|
|
267
270
|
Interpret output as follows:
|
|
268
271
|
|
|
269
272
|
- Success: `Connected linkedin: <name>` or `status` shows `+ linkedin`.
|
|
@@ -417,7 +420,7 @@ Use the exact tool names and argument keys below. Limits are optional.
|
|
|
417
420
|
| `li_mark_read` | `chat_id` required | Explicitly clear one LinkedIn chat's unread state. Reading never clears unread automatically. |
|
|
418
421
|
| `li_start_chat` | `member_ids` required array of 1-10 exact `ACo`, `ACw`, or `AE` provider member IDs; `text` required exact first message; `confirm`, `approval_hash`, and `dry_run:true` apply only after preview | Start one direct or group chat. Preview first, show the complete member-ID set and exact text, then repeat that exact payload with the returned approval hash only after explicit approval. SignalDash resolves every member live, blocks protected or suppressed contacts, returns an existing exact member-set chat without sending, and charges one thread-message plus aggregate action for a new chat regardless of member count. `dry_run:true` proves the full path up to the provider write without consuming approval or budget. |
|
|
419
422
|
| `li_send_message` | Manual path: `chat_id` and `text` required; `expected_watermark` optional exact 64-character watermark; `mark_read` optional boolean, default true. Secretary path: `secretary_receipt_id` alone. | Send one approved LinkedIn reply after an immediate read of that exact chat. A Secretary receipt derives the exact stored chat and text and refuses every caller-supplied override. |
|
|
420
|
-
| `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. |
|
|
423
|
+
| `li_send_invitation` | `provider_id` required; `note` optional, max 300 exact characters; `follow_up` optional, max 5000; `note_mode` optional, `follow_up` (default) or `inline`; `confirm` optional, default false | First preview one exact target, note and follow-up. By default a note is NOT put on the invitation (free accounts get notes refused with provider_http_400/422): the invitation goes out bare and the note text becomes a pending invitation follow-up, sent once as the first message after the person is proven to have accepted. `note_mode:"inline"` keeps the note on the invitation. 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. |
|
|
421
424
|
| `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. |
|
|
422
425
|
| `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. |
|
|
423
426
|
| `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. |
|
|
@@ -449,6 +452,11 @@ Use the exact tool names and argument keys below. Limits are optional.
|
|
|
449
452
|
| `wa_transcribe_voice` | `chat_id`, `message_id`, `attachment_id` all required, max 500 characters each; `backend` optional, exactly `gemini` or `whisper` | Turn one WhatsApp voice note into text through SignalDash instead of fetching provider bytes yourself. Always read the returned `backend`: `gemini` is the accurate default, `whisper-small` is the weak local fallback and mangles German with English terms mixed in, and a fallback also carries `fallback_reason`. Non-audio attachments are refused with `415 not_audio`; an unknown backend with `400 unknown_backend`; a transcription that exceeds its time limit returns `504 transcription_timeout` with the stored audio path. |
|
|
450
453
|
| `wa_start_chat` | `member_ids` required array of 1-10 exact `@s.whatsapp.net` or `@lid` provider member IDs; `text` required exact first message; optional `subject` is the group name of a new group chat (2+ members, at most 100 characters, bound into the approval; there is no rename afterwards, so always pass it for a group); `confirm`, `approval_hash`, and `dry_run:true` apply only after preview | Start one direct or group chat under the same exact approval, live member resolution, protected-contact, duplicate-set, and readback contract as LinkedIn. If an `@lid` resolves to another canonical ID, repeat the preview with the returned `resolved_member_id`; SignalDash never substitutes an unapproved identity. A new chat spends one WhatsApp/email send action regardless of member count. An existing exact member-set chat is returned without sending. |
|
|
451
454
|
| `wa_send_message` | `chat_id` required; `text` optional only when a file is attached, max 5000 characters; `attachments` optional array of up to 4 exact `{filename, content_type, content_base64}` files, at most 16 MiB per file and 16 MiB per message, types `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `application/pdf`, `text/csv`, `text/plain`, `application/json`, `application/zip`, xlsx; `expected_watermark` optional exact 64-character watermark; `mark_read` optional boolean, default true | Send one approved reply, one approved file, or both, in an existing WhatsApp conversation after an immediate re-read. Own outbound additions do not invalidate the read; inbound additions or mutations return `new_messages`, `changed_kind`, and `current_watermark`. Pass `expected_watermark` to bind the send to the exact reviewed state and `mark_read:false` to preserve unread instead of clearing it after the confirmed send. A file spends the same daily send budget and is recorded the same way as a text message; there is no separate attachment budget. A call carrying neither text nor an attachment is refused with `400 text_or_attachment_required`. Attachments are checked before anything is reserved, so a refusal costs no send: `400 unsupported_attachment_type`, `400 attachment_too_large`, `400 attachments_too_large`, `400 too_many_attachments`, `400 malformed_attachment_base64`, `400 invalid_attachment_filename`, and `413 request_too_large` when the whole body is too big to read. Nothing is ever truncated or dropped silently. The same caption with the same file is refused as `409 duplicate_send`; the same caption with a different file is a different message and goes through. If a send times out or the provider never confirms it, the message may still have been delivered: an identical retry is refused with `409 send_outcome_unknown`. Read the chat AGAIN, and ONLY if the message is genuinely absent, resend the identical payload with `confirm_resend:true`. The re-read is enforced, not advisory: a `confirm_resend` whose most recent read of that chat predates the failed attempt is refused with `428 reread_after_failed_send_required`, because a read taken before the attempt cannot show whether the message arrived. A re-read failure remains `502 thread_preflight_unavailable`, and a legacy proof with no watermark remains `428 read_before_send_required`; neither costs send budget. LinkedIn messages carry text only. |
|
|
455
|
+
| `ig_list_chats` | `limit` integer 1-100, default 20; `cursor` optional; `search` optional string, max 200 characters; `max_scan` optional integer 1-500, default 200; `unread` optional boolean | Find an existing Instagram DM conversation and its exact `chat_id` on the Instagram account bound to this user. A search filters over a bounded scan: read `search.exhaustive` before concluding anything, and continue from `cursor`. |
|
|
456
|
+
| `ig_read_messages` | `chat_id` required; `limit` 1-100, default 30 | Read one Instagram DM thread before summarizing, drafting, or sending. Returns `current_watermark`. Reading never clears unread. |
|
|
457
|
+
| `ig_mark_read` | `chat_id` required | Explicitly clear one Instagram chat's unread state. |
|
|
458
|
+
| `ig_get_attachment` | `chat_id`, `message_id`, `attachment_id` all required, max 500 characters each | Download one attachment of one Instagram DM in a chat this account owns, stored on the SignalDash host. |
|
|
459
|
+
| `ig_send_message` | `chat_id` and `text` required, text max 5000 characters; `expected_watermark` optional; `mark_read` optional boolean, default true; `confirm_resend:true` only after a re-read following `409 send_outcome_unknown` | Send one approved Instagram DM, text only, in an existing chat after an immediate re-read. Same guards as `wa_send_message`: `428 read_before_send_required`, `409 thread_changed`, `409 duplicate_send`, `409 send_outcome_unknown`, `409 contact_suppressed`. Before writing it confirms the account bound to this user is a ready Instagram account (`409 instagram_account_unverified` otherwise). A thread whose read page holds no message of this account counts as a NEW conversation, which has a much smaller cap. Budget refusals are `429` with an exact code: `instagram_pacing_cooldown`, `instagram_hourly_limit`, `instagram_daily_limit`, `instagram_weekly_limit`, `instagram_new_chat_daily_limit`, `instagram_new_chat_weekly_limit`, `instagram_account_paused`; each carries `retry_after_seconds` and the full `instagram_budget`. Never retry early or work around one. |
|
|
452
460
|
| `wa_delete_message` | `chat_id`, `message_id` both required, max 500 characters each | Retract one message THIS account sent, in a chat this account owns. The exact `message_id` comes from `wa_read_messages`. Irreversible and never retried: someone else's message is refused with `403 message_not_own`, a message outside this chat with `403 message_forbidden`, and a delete already recorded for this exact chat and message with `409 duplicate_delete`. Deletes spend their own daily budget, so `429 rate_limit_exceeded` here never means you are out of sends. SignalDash re-reads after the delete and returns success only when `deleted:1` or a genuine 404 proves the message gone. A present row or failed readback returns `502 deleted_unconfirmed`, records an unknown outcome, and must not be retried. |
|
|
453
461
|
| `wa_delete_messages` | `messages` required array of 1-200 exact `{chat_id, message_id}` objects | Retract several messages this account sent. Same ownership, budget and audit path as `wa_delete_message`, executed strictly one at a time with a pause between them, never in parallel. Always read the per-entry `ok`, `code` and `error`: a partial result is normal. Entries the batch never reached before its time limit come back with `skipped:true` and `code:batch_deadline`, and were not attempted; resend exactly those to resume. |
|
|
454
462
|
| `email_list` | `limit` integer 1-100, default 20; `cursor` optional, max 4096 | List the newest message in each recent email thread and obtain `thread_id`. The response carries a `cursor`; pass it back to read the next page, and omit it for the first. |
|
|
@@ -478,6 +486,9 @@ Use the exact tool names and argument keys below. Limits are optional.
|
|
|
478
486
|
| `sd_schedule_message` | `channel` required, `whatsapp` or `linkedin`; `chat_id` required, max 500; `text` required, max 5000; `scheduled_at` required offset-qualified ISO date-time from 60 seconds to 365 days ahead; `confirm:true` required | Schedule one exact message into one chat you have already read. Read that exact chat first, at `limit` 10 or more on LinkedIn: SignalDash records what the thread looked like and refuses at send time if the conversation moved. Text only; attachments are refused rather than dropped. One message at one time, never a sequence. |
|
|
479
487
|
| `sd_scheduled_messages` | `state` optional, one of `scheduled`, `executing`, `sent`, `cancelled`, `failed`, `needs_review`; `channel` optional | List only this authenticated user's scheduled messages and their durable states. Returns every matching row, unpaginated, and always reports `needs_review_count` outside your filter. |
|
|
480
488
|
| `sd_cancel_scheduled_message` | `id` required UUID; `confirm:true` required | Cancel one exact message while its state is still `scheduled`. It cannot stop one already being sent, and it cannot unsend one that has been sent. |
|
|
489
|
+
| `sd_invitation_followups` | `state` optional: `pending`, `sending`, `sent`, `cancelled`, `expired`, `needs_review` | List invitation follow-ups and their durable states, with `needs_review_count`. Acceptance is proven hourly per pending row (invitation gone from a fully read sent list AND first-degree, or a chat exists); the send goes through the paced `li_send_message` write path once. If they already wrote, or the thread holds an outbound message, nothing is sent and the row becomes `needs_review`. Not accepted within 30 days: `expired`. |
|
|
490
|
+
| `sd_invitation_followup_create` | `provider_id`, `text` required; `invitation_id` optional; `confirm` optional | Register a follow-up for an invitation that was already sent. Preview proves a pending sent invitation to this exact person; after approval repeat the identical call with `confirm:true`. One pending follow-up per person. |
|
|
491
|
+
| `sd_invitation_followup_cancel` | `id` required UUID; `confirm:true` required | Cancel one exact follow-up while it is still `pending`. It cannot stop or unsend one being sent or sent. |
|
|
481
492
|
| `sd_followup_create` | `channel`, exact `thread_id`, task `context`, and offset-qualified `due_at` required; optional completion condition, auto-close choice, recipient label, origin, source message, and baseline | Record one independent task inside a LinkedIn, WhatsApp, email, or Slack conversation. Multiple different tasks may remain active in the same thread. Monitoring only: this never sends. |
|
|
482
493
|
| `sd_followups` | optional `status`, `channel`, and `overdue` filters | Reconcile and list task-level expectations. A reply either completes an explicitly auto-close task or moves it to `needs_review`; it never clears unrelated tasks in the same thread. |
|
|
483
494
|
| `sd_followup_cancel` | `id` required UUID; `confirm:true` required | Cancel one exact active task without deleting communication history or sending anything. |
|
|
@@ -705,6 +716,29 @@ time and work window after a campaign action has established them. The
|
|
|
705
716
|
scheduler, persisted tenant-breaker, active-batch, and target-count fields
|
|
706
717
|
reflect the current invitation-batch runtime.
|
|
707
718
|
|
|
719
|
+
### Instagram DM budget
|
|
720
|
+
|
|
721
|
+
Instagram has its own budget per connected Instagram account. It never spends
|
|
722
|
+
LinkedIn lanes or the WhatsApp/email send lane, and `sd_budget_status` reports
|
|
723
|
+
it under `instagram`, so "my action budget" always includes Instagram once it
|
|
724
|
+
is connected. Defaults:
|
|
725
|
+
|
|
726
|
+
- 50 sends per UTC day, of which at most 15 may open a new conversation;
|
|
727
|
+
- 250 sends and 60 new conversations per UTC week, reset Monday 00:00 UTC;
|
|
728
|
+
- 10 sends per rolling hour;
|
|
729
|
+
- at least 45 seconds between sends, plus 0 to 30 seconds of random jitter;
|
|
730
|
+
- warm-up after connecting: first 24 hours 10 replies and no new conversation,
|
|
731
|
+
days 1 to 6 20 sends and 5 new conversations, days 7 to 13 35 sends and 10
|
|
732
|
+
new conversations, full limits from day 14;
|
|
733
|
+
- a provider 403 or 429 pauses Instagram sends for 24 hours.
|
|
734
|
+
|
|
735
|
+
A new conversation is a send into a thread whose read page holds no message
|
|
736
|
+
from this account (a cold DM or a first answer to a message request). Replies
|
|
737
|
+
are everything else. Prefer replies; never start cold Instagram outreach
|
|
738
|
+
without the human approving each exact message. An operator can set a
|
|
739
|
+
per-account override, but the hard ceilings stay at 100 per day, 10 per hour
|
|
740
|
+
and 40 new conversations per day.
|
|
741
|
+
|
|
708
742
|
### Opt-in invitation auto-accept
|
|
709
743
|
|
|
710
744
|
`sd_settings_get` is the single read surface for persistent SignalDash
|
|
@@ -1602,6 +1636,24 @@ looping `wa_delete_message` yourself: it paces them and never runs two at once.
|
|
|
1602
1636
|
Read the per-entry results, and resend only the entries marked
|
|
1603
1637
|
`code:batch_deadline`, which were never attempted.
|
|
1604
1638
|
|
|
1639
|
+
### Flow 3b: find and reply to an Instagram DM
|
|
1640
|
+
|
|
1641
|
+
Instagram is connected with `signaldash connect instagram`, which prints a
|
|
1642
|
+
link the human opens in their own browser to sign in to Instagram. The account
|
|
1643
|
+
is bound to that human's own SignalDash user, never to the workspace.
|
|
1644
|
+
|
|
1645
|
+
1. `sd_budget_status` and read `instagram`: remaining sends, remaining new
|
|
1646
|
+
conversations, warm-up stage, and `next_send_allowed_at`.
|
|
1647
|
+
2. `ig_list_chats` with `search` or `unread:true` to find the exact `chat_id`.
|
|
1648
|
+
3. `ig_read_messages` for that chat. Draft the reply and show the human the
|
|
1649
|
+
exact text.
|
|
1650
|
+
4. After approval, `ig_send_message` with `chat_id`, `text` and the
|
|
1651
|
+
`expected_watermark` from the read. On `409 thread_changed`, show the new
|
|
1652
|
+
messages and redraft. On any `429 instagram_*` code, stop and report the
|
|
1653
|
+
cap and `retry_after_seconds`; never retry early.
|
|
1654
|
+
5. `ig_mark_read` only when the human wants the chat cleared and the send did
|
|
1655
|
+
not already clear it.
|
|
1656
|
+
|
|
1605
1657
|
### Flow 4: send, accept, or withdraw one LinkedIn invitation
|
|
1606
1658
|
|
|
1607
1659
|
For a new invitation:
|