@floomhq/signaldash 0.6.0 → 0.8.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 +2 -0
- package/bin/sd.mjs +108 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ URL and scan the live QR code. The CLI prints `Connected linkedin` or
|
|
|
25
25
|
The invite code is single-use. Login stores a SignalDash user token in
|
|
26
26
|
`~/.signaldash/config.json`; the Unipile access key remains on the SignalDash
|
|
27
27
|
server. Set `SIGNALDASH_HOME` to keep the local config in another directory.
|
|
28
|
+
Sessions expire after 90 days by default. Run `npx -y @floomhq/signaldash logout`
|
|
29
|
+
to revoke the current session and remove its local token.
|
|
28
30
|
|
|
29
31
|
If automatic account detection times out, the CLI prints the manual claim
|
|
30
32
|
command for the provider:
|
package/bin/sd.mjs
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
// bearer token. Never holds a Unipile key, never calls Unipile directly. The MCP
|
|
4
4
|
// tools an agent uses all proxy through the backend, so agents access channels
|
|
5
5
|
// THROUGH SignalDash, not around it.
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
} from "node:fs";
|
|
7
13
|
import { homedir } from "node:os";
|
|
8
14
|
import { createInterface } from "node:readline";
|
|
9
15
|
import { fileURLToPath } from "node:url";
|
|
@@ -30,8 +36,10 @@ function loadCfg() {
|
|
|
30
36
|
}
|
|
31
37
|
function saveCfg(c) {
|
|
32
38
|
const config = configPaths();
|
|
33
|
-
mkdirSync(config.directory, { recursive: true });
|
|
39
|
+
mkdirSync(config.directory, { recursive: true, mode: 0o700 });
|
|
40
|
+
chmodSync(config.directory, 0o700);
|
|
34
41
|
writeFileSync(config.file, JSON.stringify(c, null, 2), { mode: 0o600 });
|
|
42
|
+
chmodSync(config.file, 0o600);
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
async function api(path, body, { auth = true, method = "POST" } = {}) {
|
|
@@ -67,6 +75,30 @@ export async function cmdLogin(code, backend, dependencies = {}) {
|
|
|
67
75
|
log("Logged in to SignalDash. Token stored (no channel keys on your side).");
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
export async function cmdLogout(dependencies = {}) {
|
|
79
|
+
const request = dependencies.request || api;
|
|
80
|
+
const log = dependencies.log || console.log;
|
|
81
|
+
const error = dependencies.error || console.error;
|
|
82
|
+
const cfg = loadCfg();
|
|
83
|
+
if (!cfg.token) {
|
|
84
|
+
log("Already logged out.");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const response = await request("/logout", {});
|
|
88
|
+
if (response.status !== 200 && response.status !== 401) {
|
|
89
|
+
error("logout failed:", response.json.error || response.status);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
delete cfg.token;
|
|
94
|
+
saveCfg(cfg);
|
|
95
|
+
log(
|
|
96
|
+
response.status === 200
|
|
97
|
+
? "Logged out of SignalDash. The session was revoked."
|
|
98
|
+
: "Local session cleared. The server session was already invalid.",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
export async function cmdConnect(provider, dependencies = {}) {
|
|
71
103
|
const request = dependencies.request || api;
|
|
72
104
|
const sleep = dependencies.wait || wait;
|
|
@@ -80,7 +112,9 @@ export async function cmdConnect(provider, dependencies = {}) {
|
|
|
80
112
|
}
|
|
81
113
|
const r = await request(`/connect/${provider}`, {});
|
|
82
114
|
if (r.status >= 300) {
|
|
83
|
-
error(
|
|
115
|
+
error(r.json.error === "login required"
|
|
116
|
+
? "Not logged in. Run: signaldash login <invite-code>"
|
|
117
|
+
: "connect failed: " + (r.json.error || r.status));
|
|
84
118
|
process.exitCode = 1;
|
|
85
119
|
return;
|
|
86
120
|
}
|
|
@@ -222,17 +256,86 @@ export async function cmdSetup(code, dependencies = {}) {
|
|
|
222
256
|
log("\n " + chalk.dim("Try: ") + chalk.white('"list my recent whatsapp chats and draft replies"') + "\n");
|
|
223
257
|
}
|
|
224
258
|
|
|
259
|
+
|
|
260
|
+
export async function cmdStatus(dependencies = {}) {
|
|
261
|
+
const log = dependencies.log || console.log;
|
|
262
|
+
const { chalk } = await ui();
|
|
263
|
+
const cfg = loadCfg();
|
|
264
|
+
if (!cfg.token) { log(" Not logged in. Run: " + chalk.cyan("signaldash login <invite-code>")); return; }
|
|
265
|
+
const request = dependencies.request || api;
|
|
266
|
+
log("");
|
|
267
|
+
log(" " + chalk.bold("SignalDash") + chalk.dim(" " + (cfg.backend || DEFAULT_BACKEND)));
|
|
268
|
+
for (const provider of ["linkedin", "whatsapp"]) {
|
|
269
|
+
const r = await request(`/connect/${provider}/status`, undefined, { method: "GET" });
|
|
270
|
+
const ok = r.status === 200 && r.json.connected;
|
|
271
|
+
log(" " + (ok ? chalk.green("+") : chalk.dim("-")) + " " + provider.padEnd(9) +
|
|
272
|
+
(ok ? chalk.dim(r.json.name || "connected") : chalk.dim("not connected")));
|
|
273
|
+
}
|
|
274
|
+
log("");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function cmdConnections(outPath, dependencies = {}) {
|
|
278
|
+
const log = dependencies.log || console.log;
|
|
279
|
+
const request = dependencies.request || api;
|
|
280
|
+
const { chalk, ora } = await ui();
|
|
281
|
+
const { writeFileSync: wf, readFileSync: rf, existsSync: ex } = await import("node:fs");
|
|
282
|
+
const file = outPath || "linkedin-connections.csv";
|
|
283
|
+
|
|
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 {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const spin = ora({ text: "Fetching connections (paced, this is deliberate)...", indent: 2 }).start();
|
|
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 }));
|
|
310
|
+
}
|
|
311
|
+
spin.succeed(`${rows.length} connections`);
|
|
312
|
+
|
|
313
|
+
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 {}
|
|
318
|
+
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."));
|
|
321
|
+
}
|
|
322
|
+
|
|
225
323
|
export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
226
324
|
const [cmd, a, b, c] = argv;
|
|
227
325
|
const log = dependencies.log || console.log;
|
|
228
326
|
if (cmd === "setup") await cmdSetup(a, dependencies);
|
|
229
|
-
else if (cmd && /^[0-9a-f]{8,}$/i.test(cmd) && !["login","connect","mcp","skill"].includes(cmd)) await cmdSetup(cmd, dependencies);
|
|
327
|
+
else if (cmd && /^[0-9a-f]{8,}$/i.test(cmd) && !["login","logout","connect","mcp","skill"].includes(cmd)) await cmdSetup(cmd, dependencies);
|
|
230
328
|
else if (cmd === "login") await cmdLogin(a, b === "--backend" ? c : undefined, dependencies);
|
|
329
|
+
else if (cmd === "logout") await cmdLogout(dependencies);
|
|
231
330
|
else if (cmd === "connect" && b === "claim") await cmdClaim(a, c, dependencies);
|
|
232
331
|
else if (cmd === "connect") await cmdConnect(a, dependencies);
|
|
233
332
|
else if (cmd === "mcp") await runMcp(dependencies);
|
|
333
|
+
else if (cmd === "status") await cmdStatus(dependencies);
|
|
334
|
+
else if (cmd === "connections" || (cmd === "export" && a === "connections")) await cmdConnections(cmd === "export" ? b : a, dependencies);
|
|
335
|
+
else if (cmd === "--version" || cmd === "-v") log("0.7.0");
|
|
336
|
+
else if (cmd && cmd !== "help" && !/^[0-9a-f]{8,}$/i.test(cmd)) { (dependencies.error || console.error)(`unknown command: ${cmd}`); printHelp(log); process.exitCode = 1; }
|
|
234
337
|
else if (cmd === "skill") await cmdSkill(dependencies);
|
|
235
|
-
else log
|
|
338
|
+
else printHelp(log);
|
|
236
339
|
}
|
|
237
340
|
|
|
238
341
|
function isDirectRun() {
|