@floomhq/signaldash 0.8.0 → 0.10.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.
Files changed (2) hide show
  1. package/bin/sd.mjs +28 -34
  2. package/package.json +1 -1
package/bin/sd.mjs CHANGED
@@ -164,10 +164,14 @@ const TOOLS = [
164
164
  { name: "wa_list_chats", ch: "wa", action: "list_chats", description: "List your WhatsApp chats." },
165
165
  { name: "wa_read_messages", ch: "wa", action: "read", description: "Read messages in a WhatsApp chat. args: chat_id" },
166
166
  { name: "wa_send_message", ch: "wa", action: "send", description: "Send a WhatsApp message (rate-safe). args: chat_id, text" },
167
+ { name: "li_my_posts", path: "/li/posts", description: "List the user's own LinkedIn posts with engagement counts (reactions, comments, impressions). args: limit" },
168
+ { name: "li_post_reactions", path: "/li/post_reactions", description: "Who reacted to a post — name + headline. These are warm inbound signals. args: post_id, limit" },
169
+ { name: "li_post_comments", path: "/li/post_comments", description: "Comments on a post, with author. args: post_id, limit" },
170
+ { name: "li_draft_post", path: "/li/create_post", description: "Draft a LinkedIn post. Returns the draft WITHOUT publishing. Publishing requires the human to approve and re-send with publish:true. args: text" },
167
171
  ];
168
172
  function mcpTool(name) {
169
173
  return { name, description: TOOLS.find(t => t.name === name).description,
170
- inputSchema: { type: "object", properties: { chat_id: { type: "string" }, text: { type: "string" }, limit: { type: "number" } } } };
174
+ inputSchema: { type: "object", properties: { chat_id: { type: "string" }, text: { type: "string" }, limit: { type: "number" }, post_id: { type: "string" }, member_id: { type: "string" }, publish: { type: "boolean" } } } };
171
175
  }
172
176
  export async function runMcp(dependencies = {}) {
173
177
  const input = dependencies.input || process.stdin;
@@ -278,46 +282,36 @@ export async function cmdConnections(outPath, dependencies = {}) {
278
282
  const log = dependencies.log || console.log;
279
283
  const request = dependencies.request || api;
280
284
  const { chalk, ora } = await ui();
281
- const { writeFileSync: wf, readFileSync: rf, existsSync: ex } = await import("node:fs");
285
+ const { writeFileSync: wf } = await import("node:fs");
282
286
  const file = outPath || "linkedin-connections.csv";
283
287
 
284
- // Resume support: keep the cursor next to the CSV so a large network is
285
- // fetched over several paced passes instead of one burst (LinkedIn detection
286
- // is behavioural; bursts are what get accounts flagged).
287
- const stateFile = file + ".state.json";
288
- let cursor = null, rows = [];
289
- if (ex(stateFile)) {
290
- try {
291
- const prev = JSON.parse(rf(stateFile, "utf8"));
292
- cursor = prev.cursor || null; rows = prev.rows || [];
293
- if (cursor) log(" " + chalk.dim(`resuming from previous run (${rows.length} already fetched)`));
294
- } catch {}
288
+ // Background drip-sync: one page (~100 connections) per ~90s, server-side.
289
+ // A burst export of a large network looks robotic to LinkedIn; this paces it
290
+ // like a human and survives restarts, so the user just waits for the file.
291
+ const start = await request("/li/connections/sync", {});
292
+ if (start.status >= 300) {
293
+ (dependencies.error || console.error)(start.json.error || "could not start sync");
294
+ process.exitCode = 1; return;
295
295
  }
296
-
297
- const spin = ora({ text: "Fetching connections (paced, this is deliberate)...", indent: 2 }).start();
296
+ const spin = ora({ text: "Syncing connections (paced to keep your account safe)...", indent: 2 }).start();
297
+ let done = false;
298
298
  for (;;) {
299
- const r = await request("/li/connections", { cursor, max_pages: 10 });
300
- if (r.status >= 300) {
301
- spin.fail(r.json.error || "failed");
302
- if (rows.length) { wf(stateFile, JSON.stringify({ cursor, rows })); log(" " + chalk.dim("progress saved; run again to resume")); }
303
- process.exitCode = 1; return;
304
- }
305
- rows = rows.concat(r.json.connections || []);
306
- cursor = r.json.next_cursor;
307
- spin.text = `Fetched ${rows.length} connections...`;
308
- if (!cursor) break;
309
- wf(stateFile, JSON.stringify({ cursor, rows }));
299
+ const st = await request("/li/connections/sync", undefined, { method: "GET" });
300
+ const s2 = st.json || {};
301
+ spin.text = `Synced ${s2.fetched ?? 0} connections (${s2.pages ?? 0} pages)...`;
302
+ if (s2.status === "complete") { done = true; break; }
303
+ if (s2.status === "failed" || s2.status === "paused") { spin.warn(`sync ${s2.status}: ${s2.error || ""}`); break; }
304
+ await new Promise(r => setTimeout(r, 15000));
310
305
  }
311
- spin.succeed(`${rows.length} connections`);
312
-
306
+ const dl = await request("/li/connections/download", undefined, { method: "GET" });
307
+ const rows = (dl.json && dl.json.connections) || [];
308
+ if (!rows.length) { spin.fail("nothing fetched yet, run again later to resume"); process.exitCode = 1; return; }
309
+ if (done) spin.succeed(`${rows.length} connections`);
313
310
  const esc = v => `"${String(v ?? "").replace(/"/g, '""')}"`;
314
- const csv = ["name,headline,public_id,profile_url,connected_at",
315
- ...rows.map(c => [c.name, c.headline, c.public_id, c.profile_url, c.connected_at].map(esc).join(","))].join("\n");
316
- wf(file, csv);
317
- try { const { unlinkSync } = await import("node:fs"); if (ex(stateFile)) unlinkSync(stateFile); } catch {}
311
+ wf(file, ["name,headline,public_id,profile_url,connected_at",
312
+ ...rows.map(c => [c.name, c.headline, c.public_id, c.profile_url, c.connected_at].map(esc).join(","))].join("\n"));
318
313
  log(" " + chalk.green("+") + " saved " + chalk.cyan(file));
319
- log(" " + chalk.dim("Tip: LinkedIn's own export (Settings > Get a copy of your data) is zero-risk"));
320
- log(" " + chalk.dim("and includes emails. Both work; that one never touches the API."));
314
+ if (!done) log(" " + chalk.dim("sync still running; run this again later to get the rest"));
321
315
  }
322
316
 
323
317
  export async function main(argv = process.argv.slice(2), dependencies = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/signaldash",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Secure LinkedIn and WhatsApp MCP access for AI agents",
5
5
  "type": "module",
6
6
  "bin": {