@floomhq/signaldash 0.6.0 → 0.7.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 (3) hide show
  1. package/README.md +2 -0
  2. package/bin/sd.mjs +96 -5
  3. 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 { readFileSync, realpathSync, writeFileSync, mkdirSync } from "node:fs";
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("connect failed:", r.json.error || r.status);
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,74 @@ 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 error = dependencies.error || console.error;
280
+ const request = dependencies.request || api;
281
+ const { chalk, ora } = await ui();
282
+ const spin = ora({ text: "Fetching your LinkedIn connections...", indent: 2 }).start();
283
+ const r = await request("/li/connections", {});
284
+ if (r.status >= 300) { spin.fail(r.json.error || "failed"); process.exitCode = 1; return; }
285
+ const rows = r.json.connections || [];
286
+ spin.succeed(`${rows.length} connections`);
287
+ const { writeFileSync: wf } = await import("node:fs");
288
+ const esc = v => `"${String(v ?? "").replace(/"/g, '""')}"`;
289
+ const csv = ["name,headline,public_id,profile_url,connected_at",
290
+ ...rows.map(c => [c.name, c.headline, c.public_id, c.profile_url, c.connected_at].map(esc).join(","))].join("\n");
291
+ const file = outPath || "linkedin-connections.csv";
292
+ wf(file, csv);
293
+ log(" " + chalk.green("+") + " saved " + chalk.cyan(file));
294
+ }
295
+
296
+
297
+ function printHelp(log = console.log) {
298
+ log(`SignalDash — secure LinkedIn + WhatsApp access for your agent.
299
+
300
+ signaldash <invite-code> set up everything in one go
301
+ signaldash status show what is connected
302
+ signaldash connect linkedin|whatsapp connect a channel
303
+ signaldash connections [file.csv] export your LinkedIn connections
304
+ signaldash skill install the agent skill
305
+ signaldash mcp run the MCP server (used by your agent)
306
+ signaldash logout revoke this device
307
+
308
+ The agent reaches your channels only through SignalDash. No keys on your machine.`);
309
+ }
310
+
225
311
  export async function main(argv = process.argv.slice(2), dependencies = {}) {
226
312
  const [cmd, a, b, c] = argv;
227
313
  const log = dependencies.log || console.log;
228
314
  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);
315
+ else if (cmd && /^[0-9a-f]{8,}$/i.test(cmd) && !["login","logout","connect","mcp","skill"].includes(cmd)) await cmdSetup(cmd, dependencies);
230
316
  else if (cmd === "login") await cmdLogin(a, b === "--backend" ? c : undefined, dependencies);
317
+ else if (cmd === "logout") await cmdLogout(dependencies);
231
318
  else if (cmd === "connect" && b === "claim") await cmdClaim(a, c, dependencies);
232
319
  else if (cmd === "connect") await cmdConnect(a, dependencies);
233
320
  else if (cmd === "mcp") await runMcp(dependencies);
321
+ else if (cmd === "status") await cmdStatus(dependencies);
322
+ else if (cmd === "connections" || (cmd === "export" && a === "connections")) await cmdConnections(cmd === "export" ? b : a, dependencies);
323
+ else if (cmd === "--version" || cmd === "-v") log("0.7.0");
324
+ 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
325
  else if (cmd === "skill") await cmdSkill(dependencies);
235
- else log(`SignalDash — secure LinkedIn + WhatsApp access for your agent.\n\n signaldash login <invite-code> [--backend URL]\n signaldash connect linkedin|whatsapp\n signaldash connect linkedin|whatsapp claim <account_id>\n signaldash mcp\n signaldash skill install the agent skill\n\nThe agent reaches channels only through SignalDash. No keys on your machine.`);
326
+ else printHelp(log);
236
327
  }
237
328
 
238
329
  function isDirectRun() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/signaldash",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Secure LinkedIn and WhatsApp MCP access for AI agents",
5
5
  "type": "module",
6
6
  "bin": {