@floomhq/signaldash 0.39.7 → 0.39.9
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 +36 -2
- package/bin/sd.mjs +239 -1
- package/package.json +1 -1
- package/skills/signaldash/SKILL.md +26 -3
- package/skills/signaldash-safe-usage/SKILL.md +3 -2
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.
|
|
123
|
+
npx -y @floomhq/signaldash@0.39.9 <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
|
|
@@ -192,12 +192,17 @@ SignalDash exposes:
|
|
|
192
192
|
- `li_my_posts(limit, member_id)`
|
|
193
193
|
- `li_post_reactions(post_id, limit, cursor?)`
|
|
194
194
|
- `li_post_comments(post_id, comment_id?, resolve_reply_state?, limit, cursor?)`
|
|
195
|
-
- `li_reply_to_comment(post_id?, parent_comment_id?, trigger_comment_id?, text?, expected_watermark?, secretary_receipt_id?)`
|
|
195
|
+
- `li_reply_to_comment(post_id?, parent_comment_id?, trigger_comment_id?, text?, mentions?, expected_watermark?, secretary_receipt_id?)`
|
|
196
196
|
- `li_like_comment(post_id, parent_comment_id, comment_id, expected_watermark?)`
|
|
197
197
|
- `li_delete_message(chat_id, message_id, confirm)`
|
|
198
198
|
- `li_delete_comment(post_id, comment_id, confirm)`
|
|
199
199
|
- `li_draft_post(text, publish, scheduled_at?, content_pipeline_id?, content_pipeline_override?, mentions?, attachments?, first_comment?)`
|
|
200
200
|
- `li_set_scheduled_post_first_comment(id, first_comment, confirm)`
|
|
201
|
+
- `li_save_post_draft(text, mentions?, attachments?, first_comment?, source_key?, metadata?)`
|
|
202
|
+
- `li_post_drafts()`
|
|
203
|
+
- `li_update_post_draft(id, text, expected_version, mentions?, attachments?, first_comment?)`
|
|
204
|
+
- `li_delete_post_draft(id, expected_version, confirm)`
|
|
205
|
+
- `li_post_draft_preview(id)`
|
|
201
206
|
- `li_scheduled_posts()`
|
|
202
207
|
- `li_scheduled_post_preview(id)`
|
|
203
208
|
- `li_edit_scheduled_post(id, text, scheduled_at, mentions?, attachments?, first_comment?, confirm?, approval_hash?, expected_version?, expected_updated_at?)`
|
|
@@ -379,6 +384,35 @@ uses the same sender binding, action ledger, duplicate guard, daily budget, and
|
|
|
379
384
|
provider-warning lock as immediate publishing. An interrupted or ambiguous
|
|
380
385
|
execution fails closed and is never retried automatically.
|
|
381
386
|
|
|
387
|
+
Arbitrary saved LinkedIn drafts use a separate durable store and never enter
|
|
388
|
+
the scheduling or publishing worker. `li_save_post_draft` stores exact text,
|
|
389
|
+
up to 25 mentions, up to 20 ordered images, and an optional first comment.
|
|
390
|
+
Each image is limited to 5 MiB and one draft to 12 MiB; each tenant is limited
|
|
391
|
+
to 1,000 drafts and 100 MiB of decoded attachment bytes. Ordinary duplicate
|
|
392
|
+
drafts remain distinct records. Imports may instead supply a tenant-scoped
|
|
393
|
+
`source_key`: an identical replay returns the existing draft, while a replay
|
|
394
|
+
with different content is refused. Bounded structured `metadata` preserves the
|
|
395
|
+
source, title, storyline, readiness, hold gates, editorial notes, visual notes,
|
|
396
|
+
and revision without placing those fields in the publishable post text. List
|
|
397
|
+
responses expose image metadata rather than base64.
|
|
398
|
+
`li_update_post_draft` is a complete compare-and-swap replacement: pass the
|
|
399
|
+
current `expected_version`, and omission clears optional mentions, images, or
|
|
400
|
+
first comment. `li_delete_post_draft` requires the current version and
|
|
401
|
+
`confirm:true`. None of these operations calls LinkedIn, Unipile, Buffer, or an
|
|
402
|
+
action-budget lane.
|
|
403
|
+
|
|
404
|
+
`li_post_draft_preview` returns a short-lived, one-use
|
|
405
|
+
`https://signaldash.dev/d/<token>` link for one exact saved draft. POSTing its
|
|
406
|
+
confirmation creates a 15-minute cookie restricted to `/drafts`; the page and
|
|
407
|
+
its `/drafts/:id/attachments/:index` media route remain bound to that tenant,
|
|
408
|
+
draft, and originating live SignalDash session. Media is served byte-for-byte
|
|
409
|
+
with `private, no-store`, MIME allowlisting, `nosniff`, and a restrictive CSP.
|
|
410
|
+
The shared calendar displays unscheduled SignalDash and Buffer drafts with the
|
|
411
|
+
same cards as scheduled posts, marked by a visible `DRAFT` tag and an
|
|
412
|
+
`Unscheduled` time label. Saved SignalDash media uses separate protected
|
|
413
|
+
`/calendar/drafts/...` URLs. Drafts never enter the `items` returned by
|
|
414
|
+
`li_scheduled_posts`, preserving its existing calendar contract.
|
|
415
|
+
|
|
382
416
|
`li_scheduled_posts` is the shared planning read across posts stored by
|
|
383
417
|
SignalDash and the configured Buffer LinkedIn channel. Every item names its
|
|
384
418
|
source, and the response reports independent completeness for SignalDash,
|
package/bin/sd.mjs
CHANGED
|
@@ -1564,7 +1564,7 @@ const TOOLS = [
|
|
|
1564
1564
|
{
|
|
1565
1565
|
name: "li_reply_to_comment",
|
|
1566
1566
|
path: "/li/reply_to_comment",
|
|
1567
|
-
description: "Reply once to one exact inbound comment on the authenticated sender's own LinkedIn post. First read li_post_comments and preserve the exact post_id, parent_comment_id, trigger_comment_id, text, and returned signaldash_watermark. SignalDash re-proves post ownership, trigger identity and content, absence of an own duplicate, sender generation, budget, and provider safety immediately before writing. A 2xx is not success until bounded readback finds exactly one own reply. A Secretary approval uses secretary_receipt_id alone.",
|
|
1567
|
+
description: "Reply once to one exact inbound comment on the authenticated sender's own LinkedIn post. First read li_post_comments and preserve the exact post_id, parent_comment_id, trigger_comment_id, text, and returned signaldash_watermark. Optional mentions tag people in the reply: spell each person's name into text exactly once as a whole word and pass a matching mentions entry, exactly as li_draft_post does. LinkedIn already notifies the comment's author, so a mention is for tagging a THIRD person. SignalDash re-proves post ownership, trigger identity and content, absence of an own duplicate, sender generation, budget, and provider safety immediately before writing. A 2xx is not success until bounded readback finds exactly one own reply whose landed text equals text, so a tag whose display name does not render exactly as written is reported unverified rather than assumed sent. A Secretary approval uses secretary_receipt_id alone.",
|
|
1568
1568
|
inputSchema: {
|
|
1569
1569
|
type: "object",
|
|
1570
1570
|
properties: {
|
|
@@ -1572,6 +1572,18 @@ const TOOLS = [
|
|
|
1572
1572
|
parent_comment_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1573
1573
|
trigger_comment_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1574
1574
|
text: { type: "string", minLength: 1, maxLength: 1250 },
|
|
1575
|
+
mentions: {
|
|
1576
|
+
type: "array", maxItems: 20,
|
|
1577
|
+
description: "People to tag. Each name must appear exactly once in text as a whole word; SignalDash replaces that one occurrence with the provider's mention token. Use the person's exact LinkedIn display name, because the landed comment is verified against text character for character.",
|
|
1578
|
+
items: {
|
|
1579
|
+
type: "object",
|
|
1580
|
+
properties: {
|
|
1581
|
+
name: { type: "string", minLength: 1, maxLength: 120 },
|
|
1582
|
+
profile_id: { type: "string", minLength: 1, maxLength: 250 },
|
|
1583
|
+
},
|
|
1584
|
+
required: ["name", "profile_id"],
|
|
1585
|
+
},
|
|
1586
|
+
},
|
|
1575
1587
|
expected_watermark: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
1576
1588
|
secretary_receipt_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1577
1589
|
},
|
|
@@ -1689,12 +1701,238 @@ const TOOLS = [
|
|
|
1689
1701
|
additionalProperties: false,
|
|
1690
1702
|
},
|
|
1691
1703
|
},
|
|
1704
|
+
{
|
|
1705
|
+
name: "li_save_post_draft",
|
|
1706
|
+
path: "/li/save_post_draft",
|
|
1707
|
+
description: "Persist one arbitrary LinkedIn post draft without scheduling or publishing it. Stores exact text, ordered image bytes, mentions, and an optional first comment. Duplicate drafts are allowed. No provider call or action budget is used.",
|
|
1708
|
+
inputSchema: {
|
|
1709
|
+
type: "object",
|
|
1710
|
+
properties: {
|
|
1711
|
+
text: { type: "string", minLength: 1, maxLength: 3000 },
|
|
1712
|
+
mentions: {
|
|
1713
|
+
type: "array", maxItems: 25,
|
|
1714
|
+
items: {
|
|
1715
|
+
type: "object",
|
|
1716
|
+
properties: {
|
|
1717
|
+
name: { type: "string", minLength: 1, maxLength: 120 },
|
|
1718
|
+
profile_id: { type: "string", minLength: 1, maxLength: 250 },
|
|
1719
|
+
},
|
|
1720
|
+
required: ["name", "profile_id"],
|
|
1721
|
+
additionalProperties: false,
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
attachments: {
|
|
1725
|
+
type: "array", maxItems: 20,
|
|
1726
|
+
items: {
|
|
1727
|
+
type: "object",
|
|
1728
|
+
properties: {
|
|
1729
|
+
filename: { type: "string", minLength: 1, maxLength: 180 },
|
|
1730
|
+
content_type: { type: "string", enum: ["image/png", "image/jpeg", "image/webp", "image/gif"] },
|
|
1731
|
+
content_base64: { type: "string", minLength: 1 },
|
|
1732
|
+
},
|
|
1733
|
+
required: ["filename", "content_type", "content_base64"],
|
|
1734
|
+
additionalProperties: false,
|
|
1735
|
+
},
|
|
1736
|
+
},
|
|
1737
|
+
first_comment: { type: "string", minLength: 1, maxLength: 1250 },
|
|
1738
|
+
source_key: {
|
|
1739
|
+
type: "string",
|
|
1740
|
+
minLength: 1,
|
|
1741
|
+
maxLength: 256,
|
|
1742
|
+
description: "Optional tenant-scoped idempotency key for a durable source item. Reusing it with different content is refused.",
|
|
1743
|
+
},
|
|
1744
|
+
metadata: {
|
|
1745
|
+
type: "object",
|
|
1746
|
+
properties: {
|
|
1747
|
+
source_group: { type: "string", minLength: 1, maxLength: 100 },
|
|
1748
|
+
source_batch_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
1749
|
+
source_item_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
1750
|
+
title: { type: "string", minLength: 1, maxLength: 500 },
|
|
1751
|
+
storyline: { type: "string", minLength: 1, maxLength: 2000 },
|
|
1752
|
+
readiness: { type: "string", enum: ["ready", "held"] },
|
|
1753
|
+
hold_gates: {
|
|
1754
|
+
type: "array",
|
|
1755
|
+
maxItems: 20,
|
|
1756
|
+
items: {
|
|
1757
|
+
type: "object",
|
|
1758
|
+
properties: {
|
|
1759
|
+
code: { type: "string", minLength: 1, maxLength: 100 },
|
|
1760
|
+
note: { type: "string", minLength: 1, maxLength: 2000 },
|
|
1761
|
+
},
|
|
1762
|
+
required: ["code", "note"],
|
|
1763
|
+
additionalProperties: false,
|
|
1764
|
+
},
|
|
1765
|
+
},
|
|
1766
|
+
source: {
|
|
1767
|
+
type: "object",
|
|
1768
|
+
properties: {
|
|
1769
|
+
label: { type: "string", minLength: 1, maxLength: 500 },
|
|
1770
|
+
id: { type: "string", minLength: 1, maxLength: 500 },
|
|
1771
|
+
text: { type: "string", minLength: 1, maxLength: 12000 },
|
|
1772
|
+
},
|
|
1773
|
+
additionalProperties: false,
|
|
1774
|
+
},
|
|
1775
|
+
status: { type: "string", minLength: 1, maxLength: 100 },
|
|
1776
|
+
editorial_notes: {
|
|
1777
|
+
type: "array",
|
|
1778
|
+
maxItems: 20,
|
|
1779
|
+
items: { type: "string", minLength: 1, maxLength: 4000 },
|
|
1780
|
+
},
|
|
1781
|
+
visual: { type: "string", minLength: 1, maxLength: 4000 },
|
|
1782
|
+
revision: { type: "integer", minimum: 0 },
|
|
1783
|
+
},
|
|
1784
|
+
additionalProperties: false,
|
|
1785
|
+
},
|
|
1786
|
+
},
|
|
1787
|
+
required: ["text"],
|
|
1788
|
+
additionalProperties: false,
|
|
1789
|
+
},
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
name: "li_post_drafts",
|
|
1793
|
+
path: "/li/post_drafts",
|
|
1794
|
+
description: "List this account's durable SignalDash LinkedIn post drafts and return a fresh one-use calendar link. Attachment metadata is returned, never base64 bytes. These drafts remain separate from scheduled posts and do not appear in li_scheduled_posts items.",
|
|
1795
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1796
|
+
},
|
|
1797
|
+
{
|
|
1798
|
+
name: "li_update_post_draft",
|
|
1799
|
+
path: "/li/update_post_draft",
|
|
1800
|
+
description: "Atomically replace one complete saved LinkedIn draft using its exact UUID and expected_version. Omitted mentions, attachments, or first_comment are cleared. A stale version is refused. No provider call or action budget is used.",
|
|
1801
|
+
inputSchema: {
|
|
1802
|
+
type: "object",
|
|
1803
|
+
properties: {
|
|
1804
|
+
id: { type: "string", format: "uuid" },
|
|
1805
|
+
text: { type: "string", minLength: 1, maxLength: 3000 },
|
|
1806
|
+
mentions: {
|
|
1807
|
+
type: "array", maxItems: 25,
|
|
1808
|
+
items: {
|
|
1809
|
+
type: "object",
|
|
1810
|
+
properties: {
|
|
1811
|
+
name: { type: "string", minLength: 1, maxLength: 120 },
|
|
1812
|
+
profile_id: { type: "string", minLength: 1, maxLength: 250 },
|
|
1813
|
+
},
|
|
1814
|
+
required: ["name", "profile_id"],
|
|
1815
|
+
additionalProperties: false,
|
|
1816
|
+
},
|
|
1817
|
+
},
|
|
1818
|
+
attachments: {
|
|
1819
|
+
type: "array", maxItems: 20,
|
|
1820
|
+
items: {
|
|
1821
|
+
type: "object",
|
|
1822
|
+
properties: {
|
|
1823
|
+
filename: { type: "string", minLength: 1, maxLength: 180 },
|
|
1824
|
+
content_type: { type: "string", enum: ["image/png", "image/jpeg", "image/webp", "image/gif"] },
|
|
1825
|
+
content_base64: { type: "string", minLength: 1 },
|
|
1826
|
+
},
|
|
1827
|
+
required: ["filename", "content_type", "content_base64"],
|
|
1828
|
+
additionalProperties: false,
|
|
1829
|
+
},
|
|
1830
|
+
},
|
|
1831
|
+
first_comment: { type: "string", minLength: 1, maxLength: 1250 },
|
|
1832
|
+
expected_version: { type: "integer", minimum: 0 },
|
|
1833
|
+
},
|
|
1834
|
+
required: ["id", "text", "expected_version"],
|
|
1835
|
+
additionalProperties: false,
|
|
1836
|
+
},
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
name: "li_delete_post_draft",
|
|
1840
|
+
path: "/li/delete_post_draft",
|
|
1841
|
+
description: "Permanently delete one exact saved LinkedIn draft. Requires its UUID, current expected_version, and confirm:true. A stale version is refused. This does not delete a LinkedIn-native or scheduled post.",
|
|
1842
|
+
inputSchema: {
|
|
1843
|
+
type: "object",
|
|
1844
|
+
properties: {
|
|
1845
|
+
id: { type: "string", format: "uuid" },
|
|
1846
|
+
expected_version: { type: "integer", minimum: 0 },
|
|
1847
|
+
confirm: { type: "boolean", const: true },
|
|
1848
|
+
},
|
|
1849
|
+
required: ["id", "expected_version", "confirm"],
|
|
1850
|
+
additionalProperties: false,
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
name: "li_post_draft_preview",
|
|
1855
|
+
path: "/li/post_draft_preview",
|
|
1856
|
+
description: "Create a short-lived one-use browser link for one exact saved SignalDash LinkedIn draft. The isolated read-only page renders only that draft, its protected stored images, and its draft first comment.",
|
|
1857
|
+
inputSchema: {
|
|
1858
|
+
type: "object",
|
|
1859
|
+
properties: {
|
|
1860
|
+
id: { type: "string", format: "uuid" },
|
|
1861
|
+
},
|
|
1862
|
+
required: ["id"],
|
|
1863
|
+
additionalProperties: false,
|
|
1864
|
+
},
|
|
1865
|
+
},
|
|
1692
1866
|
{
|
|
1693
1867
|
name: "li_scheduled_posts",
|
|
1694
1868
|
path: "/li/scheduled_posts",
|
|
1695
1869
|
description: "Read the shared LinkedIn content calendar from SignalDash and the configured Buffer channel. Always inspect completeness and per-source state. Native LinkedIn scheduled posts and drafts are NOT visible because Unipile has no documented read route for them, and SignalDash does not use raw Voyager routes or linkedin.com browser access. An empty items array is never proof that the native LinkedIn calendar is empty.",
|
|
1696
1870
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1697
1871
|
},
|
|
1872
|
+
{
|
|
1873
|
+
name: "li_profile",
|
|
1874
|
+
path: "/li/profile",
|
|
1875
|
+
description: "Read one LinkedIn profile by public id or member id. Returns name, headline, public identifier, provider id, network distance and the profile picture url. Read-only, one provider call, no write. Exists because pictures were otherwise only available for people who had already reacted to or commented on your own posts, which leaves every new name in a shortlist faceless.",
|
|
1876
|
+
inputSchema: {
|
|
1877
|
+
type: "object",
|
|
1878
|
+
properties: { identifier: { type: "string", minLength: 1, maxLength: 200 } },
|
|
1879
|
+
required: ["identifier"],
|
|
1880
|
+
additionalProperties: false,
|
|
1881
|
+
},
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
name: "sd_share_list",
|
|
1885
|
+
path: "/sd/share_list",
|
|
1886
|
+
description: "Share one selection as a standalone signaldash.dev link, or list and revoke the links that exist. Pass `people` instead of `rows` for a shortlist of humans: each entry renders as a card with a photo when the provider gives one and initials when it does not, plus an optional `score` out of 100 and the `reason` that earned it. A score without a reason is a claim without evidence, so send both or neither, and put the scoring rule in `rubric` where the reader can see it. action:create returns the url ONCE and stores only its hash, so the link cannot be recovered later, only revoked. The page is a fixed snapshot: it never re-reads the source, so a list shared today does not keep disclosing tomorrow's data. action:list shows every live link with its open count; action:revoke kills one by its token.",
|
|
1887
|
+
inputSchema: {
|
|
1888
|
+
type: "object",
|
|
1889
|
+
properties: {
|
|
1890
|
+
action: { type: "string", enum: ["create", "list", "revoke"] },
|
|
1891
|
+
title: { type: "string", minLength: 1, maxLength: 200 },
|
|
1892
|
+
note: { type: "string", maxLength: 4000 },
|
|
1893
|
+
columns: { type: "array", minItems: 1, maxItems: 12, items: { type: "string", minLength: 1, maxLength: 80 } },
|
|
1894
|
+
rows: {
|
|
1895
|
+
type: "array", maxItems: 2000,
|
|
1896
|
+
items: { type: "array", items: { type: "string", maxLength: 2000 } },
|
|
1897
|
+
},
|
|
1898
|
+
people: {
|
|
1899
|
+
type: "array", maxItems: 500,
|
|
1900
|
+
items: {
|
|
1901
|
+
type: "object",
|
|
1902
|
+
properties: {
|
|
1903
|
+
name: { type: "string", minLength: 1, maxLength: 200 },
|
|
1904
|
+
headline: { type: "string", maxLength: 500 },
|
|
1905
|
+
reason: { type: "string", maxLength: 600 },
|
|
1906
|
+
url: { type: "string", maxLength: 500 },
|
|
1907
|
+
image: { type: "string", maxLength: 800 },
|
|
1908
|
+
score: { type: "number", minimum: 0, maximum: 100 },
|
|
1909
|
+
meta: { type: "array", maxItems: 4, items: { type: "string", maxLength: 80 } },
|
|
1910
|
+
org: { type: "string", maxLength: 120 },
|
|
1911
|
+
tags: { type: "array", maxItems: 5, items: { type: "string", maxLength: 28 } },
|
|
1912
|
+
factors: {
|
|
1913
|
+
type: "array", maxItems: 6,
|
|
1914
|
+
items: {
|
|
1915
|
+
type: "object",
|
|
1916
|
+
properties: {
|
|
1917
|
+
label: { type: "string", minLength: 1, maxLength: 60 },
|
|
1918
|
+
points: { type: "number" },
|
|
1919
|
+
},
|
|
1920
|
+
required: ["label"],
|
|
1921
|
+
additionalProperties: false,
|
|
1922
|
+
},
|
|
1923
|
+
},
|
|
1924
|
+
},
|
|
1925
|
+
required: ["name"],
|
|
1926
|
+
additionalProperties: false,
|
|
1927
|
+
},
|
|
1928
|
+
},
|
|
1929
|
+
rubric: { type: "string", maxLength: 1200 },
|
|
1930
|
+
token: { type: "string", minLength: 1, maxLength: 200 },
|
|
1931
|
+
list_id: { type: "string", minLength: 1, maxLength: 64 },
|
|
1932
|
+
},
|
|
1933
|
+
additionalProperties: false,
|
|
1934
|
+
},
|
|
1935
|
+
},
|
|
1698
1936
|
{
|
|
1699
1937
|
name: "li_scheduled_post_preview",
|
|
1700
1938
|
path: "/li/scheduled_post_preview",
|
package/package.json
CHANGED
|
@@ -444,13 +444,20 @@ Use the exact tool names and argument keys below. Limits are optional.
|
|
|
444
444
|
| `li_my_posts` | `limit` default 10, max 50; `member_id` optional | Find the user's latest posts and exact `social_id` values. Omit `member_id` to use the connected user's own ID. Pass `social_id`, not a different numeric `id`, to the engagement tools. |
|
|
445
445
|
| `li_post_reactions` | `post_id` required; use the exact `social_id` from `li_my_posts`; `limit` default 50, max 100; `cursor` optional | Read one bounded page of reactors. Always inspect `completeness.state`, `total`, and `next_cursor`; continue with the cursor while state is `incomplete`. Numeric ids are resolved against the user's own recent posts when possible, including ugcPost-backed multi-image posts. The provider total is reported when available and is never guessed from `li_my_posts`. A reaction does not authorize outreach. |
|
|
446
446
|
| `li_post_comments` | `post_id` required; `comment_id` optional exact parent id; `resolve_reply_state` optional boolean, not combinable with `comment_id`; use the exact `social_id` from `li_my_posts`; `limit` default 50, max 100; `cursor` optional | Read one bounded page of comments, or pass `comment_id` to read replies to that exact parent. Pass `resolve_reply_state: true` before replying to anything: each comment then carries `reply_state.replied_by_me`. Skip every comment whose `replied_by_me` is `true`, and skip every comment whose `replied_by_me` is `null`, which means unknown and never means nobody replied. Only `no_replies` and `answered_by_others` are proven unanswered. Always inspect `completeness.state`, `total`, and `next_cursor`; continue with the cursor while state is `incomplete`. Also inspect `reply_resolution.state`; `partial` or `aborted` means some comments were never resolved. Numeric ids are resolved against the user's own recent posts when possible. The provider total is reported when available and is never guessed from `li_my_posts`. |
|
|
447
|
-
| `li_reply_to_comment` | Manual path: `post_id`, `parent_comment_id`, `trigger_comment_id`, and `text` required; `expected_watermark` optional exact 64-character watermark. Secretary path: `secretary_receipt_id` alone. | Reply once to one exact inbound comment on this sender's own post after `li_post_comments`. SignalDash re-proves post ownership, the unchanged trigger, its author and parent, no own duplicate, sender generation, budget, and provider health immediately before writing. A 2xx is not success until readback finds exactly one matching own reply. |
|
|
447
|
+
| `li_reply_to_comment` | Manual path: `post_id`, `parent_comment_id`, `trigger_comment_id`, and `text` required; `mentions` optional array of `{name, profile_id}`; `expected_watermark` optional exact 64-character watermark. Secretary path: `secretary_receipt_id` alone. | Reply once to one exact inbound comment on this sender's own post after `li_post_comments`. SignalDash re-proves post ownership, the unchanged trigger, its author and parent, no own duplicate, sender generation, budget, and provider health immediately before writing. A 2xx is not success until readback finds exactly one matching own reply. Each mention name must appear exactly once in `text` as a whole word; SignalDash swaps that occurrence for the provider's token, and every tagged person is checked against suppression and protection exactly like the person being replied to. Because the landed comment is compared against `text` character for character, use the person's exact LinkedIn display name or the write is reported unverified. |
|
|
448
448
|
| `li_like_comment` | `post_id`, `parent_comment_id`, and `comment_id` required; `expected_watermark` optional exact 64-character watermark | Like once one exact inbound comment on this sender's own post after `li_post_comments`. SignalDash re-proves post ownership, the unchanged comment, its author and parent, no own like, sender generation, budget, and provider health immediately before writing. A 2xx is not success until bounded readback finds exactly one own like. |
|
|
449
449
|
| `li_delete_message` | `chat_id`, `message_id`, and `confirm:true` required | Remediate one exact own LinkedIn message within 60 minutes of sending. SignalDash proves account, exact chat, own authorship, timestamp eligibility, a separate remediation budget, and the provider's post-delete state. Success requires `deleted:1` or a genuine 404. A present row or failed readback returns `502 deleted_unconfirmed`, locks the sender, and is never retried automatically. This cannot undo delivery, reading, or notifications and never relaxes a send gate. |
|
|
450
450
|
| `li_delete_comment` | `post_id`, `comment_id`, and `confirm:true` required | Currently unavailable: the fixture-tested Unipile v2 wrapper is held at database capability state `untested` until live compatibility is proved against Federico's own removable comment. When enabled it proves own comment identity and readback. Deletion is remediation, not rollback. |
|
|
451
451
|
| `li_draft_post` | `text` required, max 3000; `publish` optional; `scheduled_at` optional offset-qualified ISO date-time; `content_pipeline_id` normally required for scheduling while that user's pipeline is enabled; `content_pipeline_override` optional exact `{reason,confirm?,approval_hash?}`; `mentions` optional array of up to 25 exact `{name,profile_id}` objects; `attachments` optional array of up to 20 exact `{filename,content_type,content_base64}` images, max 5 MiB each and 12 MiB total; `first_comment` optional, max 1250 | Create a server-confirmed draft, publish now, or persist an exact future LinkedIn post and its approved first comment. An enabled pipeline normally runs its fact, exact-text, shared-calendar, frequency, and breathing preflight. For one deliberate exception, preview the exact payload with `{reason}`, show it to the human, then repeat it with `confirm:true` and the returned single-use payload-bound `approval_hash`. The audit record preserves the reason, preview ID, and approval time; every non-pipeline provider safety guard remains active. |
|
|
452
452
|
| `li_set_scheduled_post_first_comment` | `id` required UUID; `first_comment` required, max 1250; `confirm:true` required | Attach one exact approved first comment to a scheduled post. SignalDash publishes it through the same connected account after the post and never republishes the post if the comment fails. |
|
|
453
|
+
| `li_save_post_draft` | `text` required, max 3000; `mentions` optional up to 25; `attachments` optional up to 20 exact images, max 5 MiB each and 12 MiB total; `first_comment` optional, max 1250; `source_key` optional tenant-scoped idempotency key, max 256; `metadata` optional bounded structured source/editorial context | Persist one arbitrary LinkedIn draft without scheduling or publishing. Exact ordered image bytes and ordinary duplicate drafts are retained. An identical `source_key` replay returns the existing draft; changed content under that key is refused. Tenant limits are 1,000 drafts and 100 MiB decoded attachment bytes. No provider call or action budget is used. |
|
|
454
|
+
| `li_post_drafts` | no arguments | List durable SignalDash drafts with attachment metadata, never base64, and return a fresh one-use calendar link. The calendar uses the scheduled-post card design for unscheduled SignalDash and Buffer drafts, with a visible `DRAFT` tag. Stored drafts remain separate from scheduled posts and never appear in `li_scheduled_posts.items`. |
|
|
455
|
+
| `li_update_post_draft` | `id`, complete replacement `text`, and current `expected_version` required; `mentions`, `attachments`, and `first_comment` optional | Atomically replace the complete saved draft. A stale version is refused with the current version and timestamp. Omitting an optional field clears it. No provider call occurs. |
|
|
456
|
+
| `li_delete_post_draft` | `id`, current `expected_version`, and `confirm:true` required | Permanently delete one exact saved SignalDash draft. Unknown, foreign, deleted, and replayed identifiers are indistinguishable. This cannot delete LinkedIn-native or scheduled content. |
|
|
457
|
+
| `li_post_draft_preview` | `id` required UUID | Create a short-lived, single-use `https://signaldash.dev/d/<token>` browser handoff for one exact saved draft. Its page and exact image bytes are protected by a tenant-, draft-, and originating-session-scoped cookie under `/drafts`. |
|
|
453
458
|
| `li_scheduled_posts` | no arguments | Read one shared content-calendar view across SignalDash and the configured Buffer LinkedIn channel. Inspect `completeness` and every `sources.*.state` before treating absence as an empty calendar. Native LinkedIn scheduled posts and drafts are invisible because Unipile has no documented read route for them; SignalDash does not use raw Voyager routes or linkedin.com browser access. An empty `items` array proves only that the visible sources returned no entries. |
|
|
459
|
+
| `li_profile` | `identifier` required, a public id or member id | Read one LinkedIn profile: name, headline, public identifier, provider id, network distance and the profile picture url. Read-only, one provider call, and it returns those six fields rather than the provider's whole record about a person. It exists because pictures were otherwise a by-product of engagement only: reactions and comments carry one, the stored connections carry no picture field, chats carry none, and the invitation preview refuses anyone already connected, so every new name in a shortlist stayed faceless. |
|
|
460
|
+
| `sd_share_list` | `action` create/list/revoke, plus `title`, `columns`, `rows`, optional `note`, or `token` to revoke | Share one selection as a standalone `https://signaldash.dev/l/<token>` page that anyone with the link can open. `create` returns the url ONCE and stores only its SHA-256 hash, so a lost link can be revoked but never recovered. The page is a fixed snapshot and never re-reads the source, so a list shared today does not keep disclosing tomorrow's data. Pass `people` instead of `rows` for a shortlist of humans: each renders as a card with a photo where the provider supplies one and initials where it does not, an optional `score` out of 100, and the `reason` that earned it, with the scoring rule in `rubric` so the reader can check it. For a plain table, every row must have exactly as many cells as there are columns, because a crooked table reads as a claim about the wrong person. `list` shows every live link with its non-secret `list_id` and open count, so a link whose url is lost can still be switched off and a forgotten one stays visible; `revoke` takes either the `token` or that `list_id` and kills it immediately, and a revoked token is indistinguishable from one that never existed. |
|
|
454
461
|
| `li_scheduled_post_preview` | `id` required UUID | Create a short-lived, single-use `https://signaldash.dev/p/<token>` browser handoff for one exact active SignalDash post. The isolated page includes its stored image attachments through account- and post-scoped protected media routes. |
|
|
455
462
|
| `li_edit_scheduled_post` | `id`, `text`, and offset-qualified `scheduled_at` required; full replacement `mentions` (up to 25), image `attachments` (up to 20, max 5 MiB each and 12 MiB total), and `first_comment` optional; confirmation repeats the identical payload with `confirm:true`, `approval_hash`, `expected_version`, and `expected_updated_at` from preview | Atomically replace one scheduled post in place. The UUID, creation time, provider account, and content-pipeline audit fields remain unchanged. A changed or non-scheduled row is refused, approval is single-use and expires, and no provider call occurs. Omitting an optional replacement field clears it. Duplicate identity covers the complete normalized payload, including exact attachment bytes, and only active scheduled or executing rows participate; cancelled history never blocks a replacement. |
|
|
456
463
|
| `li_cancel_scheduled_post` | `id` required UUID; `confirm:true` required | Cancel one exact post while it is still scheduled. It cannot recall an executing or published post. |
|
|
@@ -516,6 +523,11 @@ li_delete_message({"chat_id":"chat_li_7f3a","message_id":"msg_li_5c71","confirm"
|
|
|
516
523
|
li_delete_comment({"post_id":"exact-v2-post-id","comment_id":"exact-own-comment-id","confirm":true})
|
|
517
524
|
li_draft_post({"text":"Most agents need better context, not more autonomy."})
|
|
518
525
|
li_set_scheduled_post_first_comment({"id":"00000000-0000-4000-8000-000000000000","first_comment":"https://github.com/xai-org/x-algorithm","confirm":true})
|
|
526
|
+
li_save_post_draft({"text":"A durable idea with no publication date.","first_comment":"Source link goes here."})
|
|
527
|
+
li_post_drafts({})
|
|
528
|
+
li_update_post_draft({"id":"00000000-0000-4000-8000-000000000000","text":"Complete replacement copy.","expected_version":0})
|
|
529
|
+
li_post_draft_preview({"id":"00000000-0000-4000-8000-000000000000"})
|
|
530
|
+
li_delete_post_draft({"id":"00000000-0000-4000-8000-000000000000","expected_version":1,"confirm":true})
|
|
519
531
|
li_scheduled_posts({})
|
|
520
532
|
li_scheduled_post_preview({"id":"00000000-0000-4000-8000-000000000000"})
|
|
521
533
|
li_edit_scheduled_post({"id":"00000000-0000-4000-8000-000000000000","text":"Exact replacement","scheduled_at":"2026-09-07T08:00:00Z"})
|
|
@@ -568,6 +580,16 @@ comment failure never republishes the post. It fails interrupted or ambiguous
|
|
|
568
580
|
executions closed and never retries them automatically. `email_send` cannot
|
|
569
581
|
start a cold thread and must never be looped over recipients.
|
|
570
582
|
|
|
583
|
+
When the user wants to keep an arbitrary idea without choosing a publication
|
|
584
|
+
time, use `li_save_post_draft`, not `li_draft_post` with invented scheduling
|
|
585
|
+
arguments. Read it back with `li_post_drafts`; use its returned calendar link
|
|
586
|
+
for the combined read-only scheduled/draft view, or `li_post_draft_preview` for
|
|
587
|
+
one exact draft and its visuals. Updates are complete replacements and require
|
|
588
|
+
the current version, so preserve every optional field the user still wants and
|
|
589
|
+
omit only fields they explicitly want cleared. A saved draft does not grant
|
|
590
|
+
permission to schedule or publish it. Deletion requires explicit approval of
|
|
591
|
+
the exact draft and current version.
|
|
592
|
+
|
|
571
593
|
### Exact contact state and suppression
|
|
572
594
|
|
|
573
595
|
`sd_contact_state` is local and account-scoped. It makes no Unipile, LinkedIn,
|
|
@@ -1370,8 +1392,9 @@ own-post engagement actions. An aggregate 75-attempt emergency brake bounds the
|
|
|
1370
1392
|
combined lanes. Invitation sends also use the unchanged default 100-attempt
|
|
1371
1393
|
weekly policy. Separate counters do not alter randomized action spacing, the
|
|
1372
1394
|
single serialized provider queue, duplicate protection, or provider-warning
|
|
1373
|
-
locks.
|
|
1374
|
-
UTC day is refused as `duplicate_comment_text`.
|
|
1395
|
+
locks. The same exact comment reply text is allowed up to ten times per sender
|
|
1396
|
+
per UTC day; the eleventh is refused as `duplicate_comment_text`. A true resend
|
|
1397
|
+
to the SAME comment stays blocked by the idempotency key regardless.
|
|
1375
1398
|
|
|
1376
1399
|
WhatsApp and email share ONE separate daily bucket, and it is independent of
|
|
1377
1400
|
the LinkedIn one: a WhatsApp message never spends LinkedIn capacity and a
|
|
@@ -88,8 +88,9 @@ On no, call `sd_secretary_reject` for that exact disposition and send nothing.
|
|
|
88
88
|
- Let SignalDash serialize LinkedIn writes and apply action-specific jitter. Do
|
|
89
89
|
not parallelize write calls or bypass the guard. Invitation jitter completes
|
|
90
90
|
before the server's final provider preflight and action reservation.
|
|
91
|
-
-
|
|
92
|
-
|
|
91
|
+
- The same exact comment reply text is allowed up to ten times per UTC day,
|
|
92
|
+
which covers a human replying briefly and repeatedly. The eleventh is
|
|
93
|
+
refused as `duplicate_comment_text`; changing punctuation only to evade
|
|
93
94
|
that guard is prohibited.
|
|
94
95
|
- Campaign invitations use a sender-local Monday-Friday 09:00-17:00 work
|
|
95
96
|
window and a durable 90-180 second per-sender pacing interval. Timing is
|