@floomhq/signaldash 0.13.0 → 0.22.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 +88 -1
- package/bin/sd.mjs +391 -25
- package/lib/cli.js +16 -22
- package/lib/config-file.js +114 -0
- package/package.json +2 -2
- package/skills/signaldash/SKILL.md +655 -31
- package/skills/signaldash-safe-usage/SKILL.md +74 -9
package/README.md
CHANGED
|
@@ -106,6 +106,21 @@ SignalDash exposes:
|
|
|
106
106
|
- `li_list_chats`
|
|
107
107
|
- `li_read_messages(chat_id)`
|
|
108
108
|
- `li_send_message(chat_id, text)`
|
|
109
|
+
- `li_send_invitation(provider_id, note?, confirm?)`
|
|
110
|
+
- `li_invitations_received(limit?, cursor?)`
|
|
111
|
+
- `li_accept_invitation(invitation_id, confirm)`
|
|
112
|
+
- `li_invitations_sent(limit?, cursor?)`
|
|
113
|
+
- `li_withdraw_invitation(invitation_id, confirm)`
|
|
114
|
+
- `sd_contact_state(channel, identifiers, action?, reason?, confirm?)`
|
|
115
|
+
- `sd_budget_status()`
|
|
116
|
+
- `sd_settings_get()`
|
|
117
|
+
- `sd_settings_set(auto_accept_linkedin, auto_accept_linkedin_filters?, confirm)`
|
|
118
|
+
- `sd_auto_accept_status()`
|
|
119
|
+
- `li_search_connections(query, filters?, limit?)`
|
|
120
|
+
- `li_discover_people(query, filters?, limit?, confirm?)`
|
|
121
|
+
- `li_create_invitation_batch(source_label, time_zone, targets)`
|
|
122
|
+
- `li_get_invitation_batch(batch_id)`
|
|
123
|
+
- `li_cancel_invitation_batch(batch_id, approval_view_hash, confirm)`
|
|
109
124
|
- `wa_list_chats`
|
|
110
125
|
- `wa_read_messages(chat_id)`
|
|
111
126
|
- `wa_send_message(chat_id, text)`
|
|
@@ -120,6 +135,57 @@ SignalDash exposes:
|
|
|
120
135
|
Every operation runs through the hosted SignalDash backend. Agents never
|
|
121
136
|
receive the Unipile access key.
|
|
122
137
|
|
|
138
|
+
LinkedIn invitation auto-accept is off for every user until that user
|
|
139
|
+
explicitly enables it with `confirm:true`. The optional filters are an exact
|
|
140
|
+
public-identifier allowlist and description-keyword matches; when both groups
|
|
141
|
+
are configured, both must match. The single backend worker reads one bounded
|
|
142
|
+
received-invitation page, accepts at most five invitations per run and ten per
|
|
143
|
+
UTC day by default, jitters every action, and also consumes the shared daily
|
|
144
|
+
LinkedIn action budget. Three consecutive parse or provider errors disable the
|
|
145
|
+
setting. `sd_auto_accept_status` exposes accepted counts today and this week,
|
|
146
|
+
failed attempts today, remaining capacity, the stop reason, and sanitized
|
|
147
|
+
records for invitations whose provider shape could not be parsed. Both the current nested
|
|
148
|
+
`specifics.shared_secret` and legacy top-level shape are supported; secrets are
|
|
149
|
+
never returned or logged.
|
|
150
|
+
|
|
151
|
+
`li_search_connections` is the zero-cost, zero-provider-risk list-building
|
|
152
|
+
path. It searches only the authenticated user's stored connection snapshot by
|
|
153
|
+
name, headline, or company, supports company/headline-keyword/date filters, and
|
|
154
|
+
joins exact local contact state. It never calls LinkedIn, Unipile, or a paid
|
|
155
|
+
discovery API. When no snapshot exists, it points to the paced sync below
|
|
156
|
+
without starting provider work.
|
|
157
|
+
|
|
158
|
+
`li_discover_people` is the capped paid fallback for people beyond a completed
|
|
159
|
+
stored network. SignalDash runs the same role/company intent against the local
|
|
160
|
+
snapshot first and returns local matches without spending. With no local
|
|
161
|
+
matches, the first call returns an exact paid preview. An identical
|
|
162
|
+
`confirm:true` call can make one serialized HarvestAPI profile-search request.
|
|
163
|
+
That reservation consumes the preview atomically, so every later paid request
|
|
164
|
+
requires a fresh preview and approval.
|
|
165
|
+
Durable per-user pacing, per-user and tenant request caps, and a tenant-wide
|
|
166
|
+
micro-dollar cap are reserved atomically before the request. Hidden cards,
|
|
167
|
+
malformed identities, exact stored connections, and exact contact-state rows
|
|
168
|
+
already marked connected are excluded. Results are read-only planning evidence
|
|
169
|
+
and cannot become invitation-batch targets automatically.
|
|
170
|
+
|
|
171
|
+
Invitation batches are the narrow executable campaign boundary. An agent turns
|
|
172
|
+
the human's request into 1–10 exact canonical LinkedIn Classic profile URLs,
|
|
173
|
+
inclusion reasons, and optional notes, then creates an immutable durable
|
|
174
|
+
preview. The agent can inspect or cancel the batch but cannot approve it. A
|
|
175
|
+
human opens the returned absolute review URL, reauthenticates with the SignalDash
|
|
176
|
+
invite credential, selects unchecked eligible targets, and approves the exact
|
|
177
|
+
stored payload for up to 24 hours. Five failed credential attempts block that
|
|
178
|
+
batch's authentication surface for 15 minutes. The single backend worker
|
|
179
|
+
rechecks sender binding, exact identity, contact state, prior actions, chats,
|
|
180
|
+
invitations, daily/weekly budgets, weekday work window, pacing, and the tenant
|
|
181
|
+
breaker before each at-most-once write. A tenant provider-authentication
|
|
182
|
+
failure opens the persisted breaker immediately; an open breaker rejects new
|
|
183
|
+
batches and stops queued preview work before provider reads.
|
|
184
|
+
|
|
185
|
+
This is not a general campaign engine. It has no pause/resume, priority,
|
|
186
|
+
future start, recurring schedule, acceptance polling, automatic message,
|
|
187
|
+
follow-up, discovery-to-send bridge, or runtime target/text generation.
|
|
188
|
+
|
|
123
189
|
The CLI also provides a paced, resumable LinkedIn connections export:
|
|
124
190
|
|
|
125
191
|
```bash
|
|
@@ -136,16 +202,37 @@ workflow around that runtime control:
|
|
|
136
202
|
exhausted, SignalDash returns HTTP 429 with `code: "rate_limit_exceeded"` and
|
|
137
203
|
`Retry-After`. Usage is persisted server-side and resets at midnight UTC.
|
|
138
204
|
- Read the exact thread before every send.
|
|
205
|
+
- Preview an exact invitation target and note before sending it.
|
|
206
|
+
- List the exact received or sent invitation before accepting or withdrawing it.
|
|
139
207
|
- Email sends accept one recipient at a time and require a recent
|
|
140
208
|
`email_read` containing that address.
|
|
141
209
|
- Never infer a recipient from a partial name.
|
|
142
210
|
- Never send a duplicate or retry an ambiguous timeout without re-reading.
|
|
211
|
+
- Invitation auto-accept remains off until explicitly enabled, consumes both
|
|
212
|
+
its dedicated daily cap and the shared LinkedIn action cap, and disables
|
|
213
|
+
itself after three consecutive parse or provider errors.
|
|
143
214
|
- Do not parallelize sends or work around a rate limit.
|
|
215
|
+
- Do not bypass paid-discovery previews, pacing, request caps, or cost caps.
|
|
144
216
|
- Stop on a provider warning, checkpoint, restriction, HTTP 403, or HTTP 429.
|
|
145
217
|
- Avoid bulk profile reads, copied message bursts, cold-account automation,
|
|
146
218
|
and unsolicited WhatsApp messaging.
|
|
147
219
|
|
|
148
|
-
|
|
220
|
+
LinkedIn invitation writes are single-object actions. SignalDash serializes
|
|
221
|
+
them with other LinkedIn writes, requires an exact preview or list read,
|
|
222
|
+
applies the shared daily action cap, and enforces a separate 100-attempt weekly
|
|
223
|
+
policy by default. For a new invitation, jitter completes before the final
|
|
224
|
+
preflight and action reservation; that preflight verifies relationship state,
|
|
225
|
+
pending invitations in both directions, and absence of an existing one-to-one
|
|
226
|
+
chat. Provider timeouts become non-retryable `outcome_unknown` locks.
|
|
227
|
+
|
|
228
|
+
The first campaign time boundary is intentionally narrower than scheduled
|
|
229
|
+
messaging. Campaign invitations require a valid sender IANA timezone, run only
|
|
230
|
+
Monday through Friday from 09:00 to 17:00 sender-local time, and reserve a new
|
|
231
|
+
90–180 second per-sender pacing interval atomically with every attempted
|
|
232
|
+
action. Timing is checked before final provider preflight and again during the
|
|
233
|
+
write reservation. Downtime never creates a catch-up burst. Future start
|
|
234
|
+
dates, recurring schedules, automatic follow-ups, acceptance-triggered
|
|
235
|
+
messages, and multi-message sequences are not exposed.
|
|
149
236
|
|
|
150
237
|
## Development
|
|
151
238
|
|
package/bin/sd.mjs
CHANGED
|
@@ -4,15 +4,13 @@
|
|
|
4
4
|
// tools an agent uses all proxy through the backend, so agents access channels
|
|
5
5
|
// THROUGH SignalDash, not around it.
|
|
6
6
|
import {
|
|
7
|
-
chmodSync,
|
|
8
7
|
readFileSync,
|
|
9
8
|
realpathSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
mkdirSync,
|
|
12
9
|
} from "node:fs";
|
|
13
10
|
import { homedir } from "node:os";
|
|
14
11
|
import { createInterface } from "node:readline";
|
|
15
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { readConfigFile, updateConfigFile } from "../lib/config-file.js";
|
|
16
14
|
|
|
17
15
|
// Lazy-load presentation deps so `mcp` (stdio, machine-facing) stays clean/fast.
|
|
18
16
|
async function ui() {
|
|
@@ -32,28 +30,41 @@ function configPaths() {
|
|
|
32
30
|
}
|
|
33
31
|
function loadCfg() {
|
|
34
32
|
try {
|
|
35
|
-
return
|
|
33
|
+
return readConfigFile(configPaths().file);
|
|
36
34
|
} catch {
|
|
37
35
|
return {};
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
mkdirSync(config.directory, { recursive: true, mode: 0o700 });
|
|
43
|
-
chmodSync(config.directory, 0o700);
|
|
44
|
-
writeFileSync(config.file, JSON.stringify(c, null, 2), { mode: 0o600 });
|
|
45
|
-
chmodSync(config.file, 0o600);
|
|
38
|
+
function updateCfg(update) {
|
|
39
|
+
return updateConfigFile(configPaths().file, update);
|
|
46
40
|
}
|
|
47
41
|
|
|
48
|
-
async function api(
|
|
42
|
+
async function api(
|
|
43
|
+
path,
|
|
44
|
+
body,
|
|
45
|
+
{ auth = true, method = "POST", backend, token } = {},
|
|
46
|
+
) {
|
|
49
47
|
const cfg = loadCfg();
|
|
50
|
-
const
|
|
51
|
-
const
|
|
48
|
+
const targetBackend = backend || cfg.backend || DEFAULT_BACKEND;
|
|
49
|
+
const targetToken = token || cfg.token;
|
|
50
|
+
const r = await fetch(targetBackend + path, {
|
|
52
51
|
method,
|
|
53
|
-
headers: {
|
|
52
|
+
headers: {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
...(auth && targetToken ? { Authorization: `Bearer ${targetToken}` } : {}),
|
|
55
|
+
},
|
|
54
56
|
...(method === "GET" ? {} : { body: JSON.stringify(body || {}) }),
|
|
55
57
|
});
|
|
56
|
-
|
|
58
|
+
const json = await r.json().catch(() => ({}));
|
|
59
|
+
if (
|
|
60
|
+
json &&
|
|
61
|
+
typeof json === "object" &&
|
|
62
|
+
typeof json.approval_path === "string" &&
|
|
63
|
+
json.approval_path.startsWith("/")
|
|
64
|
+
) {
|
|
65
|
+
json.approval_url = new URL(json.approval_path, targetBackend).toString();
|
|
66
|
+
}
|
|
67
|
+
return { status: r.status, json };
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
const wait = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
@@ -69,10 +80,12 @@ export async function cmdLogin(code, backend, dependencies = {}) {
|
|
|
69
80
|
const request = dependencies.request || api;
|
|
70
81
|
const log = dependencies.log || console.log;
|
|
71
82
|
const error = dependencies.error || console.error;
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
const targetBackend = backend || loadCfg().backend || DEFAULT_BACKEND;
|
|
84
|
+
const r = await request(
|
|
85
|
+
"/login",
|
|
86
|
+
{ code },
|
|
87
|
+
{ auth: false, backend: targetBackend },
|
|
88
|
+
);
|
|
76
89
|
if (r.status !== 200) {
|
|
77
90
|
const msg = String(r.json.error || r.status);
|
|
78
91
|
if (/used|invalid/i.test(msg) && loadCfg().token) {
|
|
@@ -83,8 +96,12 @@ export async function cmdLogin(code, backend, dependencies = {}) {
|
|
|
83
96
|
if (/used/i.test(msg)) error("Invite codes are single-use. If you already ran this, try: signaldash status");
|
|
84
97
|
process.exitCode = 1; return;
|
|
85
98
|
}
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
updateCfg(current => ({
|
|
100
|
+
...current,
|
|
101
|
+
backend: targetBackend,
|
|
102
|
+
token: r.json.token,
|
|
103
|
+
}));
|
|
104
|
+
log(`Logged in to SignalDash (${targetBackend}).`);
|
|
88
105
|
log("Your LinkedIn/WhatsApp/email credentials live on that server, not on this");
|
|
89
106
|
log("machine, and are never exposed to your agent. Only you can see your data.");
|
|
90
107
|
}
|
|
@@ -98,14 +115,24 @@ export async function cmdLogout(dependencies = {}) {
|
|
|
98
115
|
log("Already logged out.");
|
|
99
116
|
return;
|
|
100
117
|
}
|
|
101
|
-
const response = await request(
|
|
118
|
+
const response = await request(
|
|
119
|
+
"/logout",
|
|
120
|
+
{},
|
|
121
|
+
{ backend: cfg.backend, token: cfg.token },
|
|
122
|
+
);
|
|
102
123
|
if (response.status !== 200 && response.status !== 401) {
|
|
103
124
|
error("logout failed:", response.json.error || response.status);
|
|
104
125
|
process.exitCode = 1;
|
|
105
126
|
return;
|
|
106
127
|
}
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
updateCfg(current => {
|
|
129
|
+
if (current.backend !== cfg.backend || current.token !== cfg.token) {
|
|
130
|
+
return current;
|
|
131
|
+
}
|
|
132
|
+
const next = { ...current };
|
|
133
|
+
delete next.token;
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
109
136
|
log(
|
|
110
137
|
response.status === 200
|
|
111
138
|
? "Logged out of SignalDash. The session was revoked."
|
|
@@ -190,6 +217,345 @@ const TOOLS = [
|
|
|
190
217
|
{ name: "li_list_chats", ch: "li", action: "list_chats", description: "List your LinkedIn chats." },
|
|
191
218
|
{ name: "li_read_messages", ch: "li", action: "read", description: "Read messages in a LinkedIn chat. args: chat_id" },
|
|
192
219
|
{ name: "li_send_message", ch: "li", action: "send", description: "Send a LinkedIn message (rate-safe). args: chat_id, text" },
|
|
220
|
+
{
|
|
221
|
+
name: "li_send_invitation",
|
|
222
|
+
path: "/li/send_invitation",
|
|
223
|
+
description: "Preview or send one exact LinkedIn invitation. First call without confirm, obtain approval, then repeat the exact provider_id and note with confirm:true.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
provider_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
228
|
+
note: { type: "string", maxLength: 300 },
|
|
229
|
+
confirm: { type: "boolean", default: false },
|
|
230
|
+
},
|
|
231
|
+
required: ["provider_id"],
|
|
232
|
+
additionalProperties: false,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "li_invitations_received",
|
|
237
|
+
path: "/li/invitations/received",
|
|
238
|
+
description: "List a bounded page of received LinkedIn invitations and authorize exact later accept actions.",
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: {
|
|
242
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
243
|
+
cursor: { type: "string", maxLength: 1000 },
|
|
244
|
+
},
|
|
245
|
+
additionalProperties: false,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "li_accept_invitation",
|
|
250
|
+
path: "/li/accept_invitation",
|
|
251
|
+
description: "Accept one exact received LinkedIn invitation after listing current received invitations and obtaining approval.",
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
invitation_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
256
|
+
confirm: { type: "boolean" },
|
|
257
|
+
},
|
|
258
|
+
required: ["invitation_id", "confirm"],
|
|
259
|
+
additionalProperties: false,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: "li_invitations_sent",
|
|
264
|
+
path: "/li/invitations/sent",
|
|
265
|
+
description: "List a bounded page of sent LinkedIn invitations and authorize exact later withdrawal actions.",
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: "object",
|
|
268
|
+
properties: {
|
|
269
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
270
|
+
cursor: { type: "string", maxLength: 1000 },
|
|
271
|
+
},
|
|
272
|
+
additionalProperties: false,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "li_withdraw_invitation",
|
|
277
|
+
path: "/li/withdraw_invitation",
|
|
278
|
+
description: "Withdraw one exact sent LinkedIn invitation after listing current sent invitations and obtaining approval.",
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: "object",
|
|
281
|
+
properties: {
|
|
282
|
+
invitation_id: { type: "string", minLength: 1, maxLength: 500 },
|
|
283
|
+
confirm: { type: "boolean" },
|
|
284
|
+
},
|
|
285
|
+
required: ["invitation_id", "confirm"],
|
|
286
|
+
additionalProperties: false,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "sd_contact_state",
|
|
291
|
+
path: "/sd/contact_state",
|
|
292
|
+
description: "Inspect exact per-user, per-channel contact state or add a protective suppression. SignalDash never infers cross-channel identity.",
|
|
293
|
+
inputSchema: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
channel: {
|
|
297
|
+
type: "string",
|
|
298
|
+
enum: ["linkedin", "whatsapp", "email"],
|
|
299
|
+
},
|
|
300
|
+
identifiers: {
|
|
301
|
+
type: "array",
|
|
302
|
+
minItems: 1,
|
|
303
|
+
maxItems: 8,
|
|
304
|
+
items: {
|
|
305
|
+
type: "object",
|
|
306
|
+
properties: {
|
|
307
|
+
kind: {
|
|
308
|
+
type: "string",
|
|
309
|
+
enum: [
|
|
310
|
+
"provider_id",
|
|
311
|
+
"public_identifier",
|
|
312
|
+
"profile_url",
|
|
313
|
+
"member_urn",
|
|
314
|
+
"chat_id",
|
|
315
|
+
"phone",
|
|
316
|
+
"email",
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
value: { type: "string", minLength: 1, maxLength: 1000 },
|
|
320
|
+
},
|
|
321
|
+
required: ["kind", "value"],
|
|
322
|
+
additionalProperties: false,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
action: {
|
|
326
|
+
type: "string",
|
|
327
|
+
enum: ["get", "suppress"],
|
|
328
|
+
default: "get",
|
|
329
|
+
},
|
|
330
|
+
reason: {
|
|
331
|
+
type: "string",
|
|
332
|
+
enum: [
|
|
333
|
+
"opt_out",
|
|
334
|
+
"not_interested",
|
|
335
|
+
"provider_block",
|
|
336
|
+
"manual",
|
|
337
|
+
"legal",
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
confirm: { type: "boolean", default: false },
|
|
341
|
+
},
|
|
342
|
+
required: ["channel", "identifiers"],
|
|
343
|
+
additionalProperties: false,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "sd_budget_status",
|
|
348
|
+
path: "/sd/budget_status",
|
|
349
|
+
description: "Read authoritative SignalDash LinkedIn attempt capacity for this UTC day and week across manual work and all campaigns. Reports the campaign-excluded portion without promising a reserved manual slot.",
|
|
350
|
+
inputSchema: {
|
|
351
|
+
type: "object",
|
|
352
|
+
properties: {},
|
|
353
|
+
additionalProperties: false,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "sd_settings_get",
|
|
358
|
+
path: "/sd/settings/get",
|
|
359
|
+
description: "Read this SignalDash user's persistent settings. Auto-accept is the first supported setting and is false by default for every existing user.",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {},
|
|
363
|
+
additionalProperties: false,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "sd_settings_set",
|
|
368
|
+
path: "/sd/settings/set",
|
|
369
|
+
description: "Update this SignalDash user's persistent settings. Currently supports opt-in, rate-capped LinkedIn invitation auto-accept and exact optional filters. Changing a persistent setting requires explicit approval and confirm:true.",
|
|
370
|
+
inputSchema: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {
|
|
373
|
+
auto_accept_linkedin: { type: "boolean" },
|
|
374
|
+
auto_accept_linkedin_filters: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {
|
|
377
|
+
public_identifiers: {
|
|
378
|
+
type: "array",
|
|
379
|
+
maxItems: 100,
|
|
380
|
+
items: {
|
|
381
|
+
type: "string",
|
|
382
|
+
minLength: 1,
|
|
383
|
+
maxLength: 200,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
description_keywords: {
|
|
387
|
+
type: "array",
|
|
388
|
+
maxItems: 20,
|
|
389
|
+
items: {
|
|
390
|
+
type: "string",
|
|
391
|
+
minLength: 1,
|
|
392
|
+
maxLength: 100,
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
additionalProperties: false,
|
|
397
|
+
},
|
|
398
|
+
confirm: { type: "boolean" },
|
|
399
|
+
},
|
|
400
|
+
required: ["auto_accept_linkedin", "confirm"],
|
|
401
|
+
additionalProperties: false,
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: "sd_auto_accept_status",
|
|
406
|
+
path: "/sd/auto_accept/status",
|
|
407
|
+
description: "Inspect this user's persistent auto-accept setting, filters, accepted/failed attempts today, remaining dedicated cap, repeated-error stop state, and any unparseable invitation records.",
|
|
408
|
+
inputSchema: {
|
|
409
|
+
type: "object",
|
|
410
|
+
properties: {},
|
|
411
|
+
additionalProperties: false,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: "li_search_connections",
|
|
416
|
+
path: "/li/connections/search",
|
|
417
|
+
description: "Search only your locally stored LinkedIn connections by name, headline, or company and join exact local contact state. Makes zero LinkedIn, Unipile, or paid discovery API calls.",
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: {
|
|
421
|
+
query: { type: "string", minLength: 1, maxLength: 200 },
|
|
422
|
+
filters: {
|
|
423
|
+
type: "object",
|
|
424
|
+
properties: {
|
|
425
|
+
company: { type: "string", minLength: 1, maxLength: 200 },
|
|
426
|
+
headline_keyword: {
|
|
427
|
+
type: "string",
|
|
428
|
+
minLength: 1,
|
|
429
|
+
maxLength: 200,
|
|
430
|
+
},
|
|
431
|
+
connected_after: { type: "string", format: "date" },
|
|
432
|
+
connected_before: { type: "string", format: "date" },
|
|
433
|
+
},
|
|
434
|
+
additionalProperties: false,
|
|
435
|
+
},
|
|
436
|
+
limit: {
|
|
437
|
+
type: "integer",
|
|
438
|
+
minimum: 1,
|
|
439
|
+
maximum: 100,
|
|
440
|
+
default: 20,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
required: ["query"],
|
|
444
|
+
additionalProperties: false,
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "li_discover_people",
|
|
449
|
+
path: "/li/discover_people",
|
|
450
|
+
description: "Discover public LinkedIn profile cards beyond your completed stored network through one capped HarvestAPI page. Local matches always win. First preview the exact paid request without confirm, obtain approval, then repeat with confirm:true. Each preview authorizes one provider request.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
query: {
|
|
455
|
+
type: "string",
|
|
456
|
+
minLength: 1,
|
|
457
|
+
maxLength: 200,
|
|
458
|
+
description: "Role or title to search.",
|
|
459
|
+
},
|
|
460
|
+
filters: {
|
|
461
|
+
type: "object",
|
|
462
|
+
properties: {
|
|
463
|
+
company: { type: "string", minLength: 1, maxLength: 200 },
|
|
464
|
+
location: { type: "string", minLength: 1, maxLength: 200 },
|
|
465
|
+
},
|
|
466
|
+
additionalProperties: false,
|
|
467
|
+
},
|
|
468
|
+
limit: {
|
|
469
|
+
type: "integer",
|
|
470
|
+
minimum: 1,
|
|
471
|
+
maximum: 10,
|
|
472
|
+
default: 10,
|
|
473
|
+
},
|
|
474
|
+
confirm: { type: "boolean", default: false },
|
|
475
|
+
},
|
|
476
|
+
required: ["query"],
|
|
477
|
+
additionalProperties: false,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "li_create_invitation_batch",
|
|
482
|
+
path: "/li/invitation_batch/create",
|
|
483
|
+
description: "Create one immutable LinkedIn Classic invitation-batch preview from exact user-approved profile URLs, inclusion reasons, and optional notes. The agent structures the natural-language request; the server never generates targets or text. Maximum 10 targets.",
|
|
484
|
+
inputSchema: {
|
|
485
|
+
type: "object",
|
|
486
|
+
properties: {
|
|
487
|
+
source_label: {
|
|
488
|
+
type: "string",
|
|
489
|
+
minLength: 1,
|
|
490
|
+
maxLength: 120,
|
|
491
|
+
},
|
|
492
|
+
time_zone: {
|
|
493
|
+
type: "string",
|
|
494
|
+
minLength: 1,
|
|
495
|
+
description: "Sender IANA timezone.",
|
|
496
|
+
},
|
|
497
|
+
targets: {
|
|
498
|
+
type: "array",
|
|
499
|
+
minItems: 1,
|
|
500
|
+
maxItems: 10,
|
|
501
|
+
items: {
|
|
502
|
+
type: "object",
|
|
503
|
+
properties: {
|
|
504
|
+
profile_url: {
|
|
505
|
+
type: "string",
|
|
506
|
+
format: "uri",
|
|
507
|
+
description: "Exact LinkedIn Classic https://www.linkedin.com/in/... URL.",
|
|
508
|
+
},
|
|
509
|
+
inclusion_reason: {
|
|
510
|
+
type: "string",
|
|
511
|
+
minLength: 1,
|
|
512
|
+
maxLength: 240,
|
|
513
|
+
},
|
|
514
|
+
note: {
|
|
515
|
+
type: "string",
|
|
516
|
+
maxLength: 200,
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
required: ["profile_url", "inclusion_reason"],
|
|
520
|
+
additionalProperties: false,
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
required: ["source_label", "time_zone", "targets"],
|
|
525
|
+
additionalProperties: false,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "li_get_invitation_batch",
|
|
530
|
+
path: "/li/invitation_batch/get",
|
|
531
|
+
description: "Inspect one exact immutable invitation batch, every target and exclusion, approval hash, timing, capacity, and execution result. This read authorizes a later exact cancel; MCP cannot approve.",
|
|
532
|
+
inputSchema: {
|
|
533
|
+
type: "object",
|
|
534
|
+
properties: {
|
|
535
|
+
batch_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
536
|
+
},
|
|
537
|
+
required: ["batch_id"],
|
|
538
|
+
additionalProperties: false,
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
name: "li_cancel_invitation_batch",
|
|
543
|
+
path: "/li/invitation_batch/cancel",
|
|
544
|
+
description: "Permanently cancel one freshly inspected invitation batch. Cancels only unstarted targets and cannot recall an executing invitation. Restart requires a new preview and human approval.",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
batch_id: { type: "string", minLength: 36, maxLength: 36 },
|
|
549
|
+
approval_view_hash: {
|
|
550
|
+
type: ["string", "null"],
|
|
551
|
+
pattern: "^[0-9a-f]{64}$",
|
|
552
|
+
},
|
|
553
|
+
confirm: { type: "boolean" },
|
|
554
|
+
},
|
|
555
|
+
required: ["batch_id", "approval_view_hash", "confirm"],
|
|
556
|
+
additionalProperties: false,
|
|
557
|
+
},
|
|
558
|
+
},
|
|
193
559
|
{ name: "wa_list_chats", ch: "wa", action: "list_chats", description: "List your WhatsApp chats." },
|
|
194
560
|
{ name: "wa_read_messages", ch: "wa", action: "read", description: "Read messages in a WhatsApp chat. args: chat_id" },
|
|
195
561
|
{ name: "wa_send_message", ch: "wa", action: "send", description: "Send a WhatsApp message (rate-safe). args: chat_id, text" },
|
|
@@ -253,7 +619,7 @@ export async function runMcp(dependencies = {}) {
|
|
|
253
619
|
for await (const line of rl) {
|
|
254
620
|
let msg; try { msg = JSON.parse(line); } catch { continue; }
|
|
255
621
|
const { id, method, params } = msg;
|
|
256
|
-
if (method === "initialize") reply(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "signaldash", version:
|
|
622
|
+
if (method === "initialize") reply(id, { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "signaldash", version: PACKAGE_VERSION } });
|
|
257
623
|
else if (method === "tools/list") reply(id, { tools: TOOLS.map(t => mcpTool(t.name)) });
|
|
258
624
|
else if (method === "tools/call") {
|
|
259
625
|
const t = TOOLS.find(x => x.name === params.name);
|
package/lib/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { chmod, mkdir, readFile
|
|
2
|
+
import { chmod, mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { updateConfigFile } from "./config-file.js";
|
|
5
6
|
import { readSecrets, writeSecrets } from "./secrets.js";
|
|
6
7
|
import { runMcp } from "./mcp.js";
|
|
7
8
|
import { UnipileClient, accountIsReady } from "./unipile.js";
|
|
@@ -49,16 +50,6 @@ async function ensureWorkspace(workspace) {
|
|
|
49
50
|
await chmod(workspace, 0o700);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
async function writeJsonPrivate(target, value) {
|
|
53
|
-
const temporary = `${target}.${process.pid}.tmp`;
|
|
54
|
-
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
|
55
|
-
mode: 0o600,
|
|
56
|
-
});
|
|
57
|
-
await chmod(temporary, 0o600);
|
|
58
|
-
await rename(temporary, target);
|
|
59
|
-
await chmod(target, 0o600);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
53
|
export async function readConfig(workspace, { optional = false } = {}) {
|
|
63
54
|
try {
|
|
64
55
|
return JSON.parse(
|
|
@@ -276,18 +267,21 @@ export async function connectCommand(channel, options, dependencies = {}) {
|
|
|
276
267
|
return { connected: false, hostedUrl };
|
|
277
268
|
}
|
|
278
269
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
...
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
270
|
+
updateConfigFile(
|
|
271
|
+
path.join(workspace, "config.json"),
|
|
272
|
+
latest => ({
|
|
273
|
+
...latest,
|
|
274
|
+
version: 1,
|
|
275
|
+
channels: {
|
|
276
|
+
...(latest.channels || {}),
|
|
277
|
+
[channel]: {
|
|
278
|
+
accountId: selected.id,
|
|
279
|
+
name: selected.name || null,
|
|
280
|
+
connectedAt: new Date().toISOString(),
|
|
281
|
+
},
|
|
287
282
|
},
|
|
288
|
-
},
|
|
289
|
-
|
|
290
|
-
await writeJsonPrivate(path.join(workspace, "config.json"), nextConfig);
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
291
285
|
io.stdout.write(
|
|
292
286
|
`${definition.label} connected: ${selected.name || selected.id} (${selected.id})\n`,
|
|
293
287
|
);
|